1 module plist.types.dict;
2 import plist.types.element;
3 import std.array;
4 import std.algorithm.sorting;
5 import std.string;
6 import std.algorithm.searching;
7 
8 class PlistElementDict : PlistElement {
9     void instantiate(DOMEntity!string entity) {
10         _entity = entity;
11         if (entity.type == EntityType.elementEmpty) return;
12         auto children = entity.children;
13         import std.conv : to;
14         if((children.length % 2) != 0)
15             throw new PlistParsingException("Length should be a multiple of 2, got " ~ to!string(children.length));
16 
17         for (int i = 0; i < children.length; i += 2) {
18             import plist.conv;
19             auto _key = children[i];
20             auto _val = children[i + 1];
21             if (_key.type != EntityType.elementStart)
22                 throw new PlistParsingException("Expected elementStart while parsing dict\n");
23             if (_key.name != PlistElementType.PLIST_ELEMENT_KEY)
24                 throw new PlistParsingException("Expected a <key> element before a value\n");
25             if (_key.children.length != 1)
26                 throw new PlistParsingException("Expected a valid string after key element\n");
27             if (_key.children[0].type != EntityType.text)
28                 throw new PlistParsingException("Did not get a text element after key\n");
29             string key = _key.children[0].text;
30             if (_entries.keys.canFind(key))
31                 throw new PlistParsingException("Cannot override an existing key within dict\n");
32             if (!validateDataType(_val)) 
33                 throw new PlistParsingException("Expected a valid data type for value in dict\n");
34             auto val = coerceRuntime(cast(PlistElementType)_val.name, _val);
35             _entries[key] = val;
36         }
37     }
38 
39     string type() {
40         return PlistElementType.PLIST_ELEMENT_DICT;
41     }
42 
43     ref PlistElement opIndex(string index) {
44         if (_entries.keys.canFind(index)) {
45             return _entries[index];
46         }
47         assert(0);
48     }
49         
50 
51     /* 
52        Only return true if we were able to fill all members of the field
53     */
54 
55     // Complexity is infinity
56     bool coerceToNative(T)(ref T obj) {
57         import std.traits;
58         enum field_names = FieldNameTuple!T;
59         import plist.conv;
60         import plist.types;
61         // This static foreach lets us go over all fields defined in the struct
62         bool fail = false;
63         static foreach(i, field; Fields!T) {
64             // We have to wrap this entire thing within a lambda
65             // and potentially make execution flow go ALL over the
66             // damn place, since alias is global, but within a lambda
67             // it isn't...
68             fail = () {
69                 // IF and only if the programmer has defined the corresponding key within the plist, proceed
70                 mixin("alias F = T." ~ field_names[i] ~ ";");
71                 static if (hasUDA!(F, PlistKey)) {
72                     // There should ONLY be one mapping to a Plist value
73                     static assert(getUDAs!(F, PlistKey).length == 1, "Cannot have more then one key to deserialize");
74                     static string key = getUDAs!(F, PlistKey)[0].key;
75                     // Do this check at runtime, since we populate the dict at runtime (and have no prior knowledge
76                     auto results = _entries.keys.find(key);
77                     if (!results.empty) {
78                         PlistElement element = _entries[key];
79                         // And ensure that the type that they want us to convert to (within the struct) is actually compatible
80                         // for example, PlistElementType.PLIST_ELEMENT_DATE to std.datetime.SysTime,
81                         // PlistElementType.PLIST_ELEMENT_STRING to "string", etc
82                         // SINCE the type field is defined at runtime,
83                         // we have to do this weird hack in order to get it to be compile-time
84                         // where we generate a MASSIVE branch comparing types
85 
86                         // As well, we have to check if it's a boolean,
87                         // since for some GOD awful reason, true/false is
88                         // defined as it's own type -_-
89                         if (element.type == "bool") {
90                             // Hack to ensure compilers don't try to cast every field to a boolean
91                             static if (typeIsCoercible!field(PlistElementType.PLIST_ELEMENT_BOOLEAN_TRUE)) {
92                                 enum type = "PlistElementBoolean";
93                                 mixin (type ~ " val = cast(" ~ type ~ ")element;"); 
94                                 mixin ("obj." ~ field_names[i] ~ " = val.value;");
95                                 return true;
96                             }
97                             else {
98                                 throw new PlistSerdeException("Field " ~ field_names[i] ~ " was not coercible to a " ~ element.type);
99                             }
100                         } else if (element.type == "dict") {
101                             import std.datetime : SysTime;
102                             // God have mercy on my soul for this
103                             // SysTime is also a dict, but we need to ignore it
104                             static if (is(typeof(F) == struct) && !is(typeof(F) == SysTime)) { 
105                                 // Now recurse!
106                                 PlistElementDict wrappedDict = cast(PlistElementDict)element;
107                                 return wrappedDict.coerceToNative!(typeof(F))(mixin("obj." ~ field_names[i])); 
108                             } else {
109                                 throw new PlistSerdeException("Field " ~ field_names[i] ~ " was not coercible to a " ~ element.type);
110                             }
111                         } else if (element.type == "array") {
112                             import std.range.primitives;
113                             import std.datetime : SysTime;
114                             static if (is(typeof(F) == PlistElementArray)) {
115                                 mixin ("obj." ~ field_names[i] ~ " = cast(PlistElementArray)element;");
116                                 return true; // strange that I have to bail out here
117                             }
118                             else static if (isArray!(typeof(F)) 
119                                             && isDynamicArray!(typeof(F)) 
120                                             && is(ElementType!(typeof(F)) == struct)
121                                             && !is(ElementType!(typeof(F)) == SysTime)) {
122                                 PlistElementArray arr = cast(PlistElementArray)element;
123                                 PlistElement[] elements = arr.entries();
124                                 /* Reset the length of the array to 0 */
125                                 mixin("obj." ~ field_names[i] ~ ".length = 0;");
126                                 bool passed = true;
127                                 foreach(e; elements) {
128                                     /* If it's not a dict, ignore it */
129                                     if (e.type() == "dict") {
130                                         PlistElementDict wrappedDict = cast(PlistElementDict)e;
131                                         mixin("obj." ~ field_names[i] ~ ".length++;");
132                                         passed = wrappedDict.coerceToNative!(ElementType!(typeof(F)))(
133                                                 mixin("obj." ~ field_names[i] ~ "[$ - 1]")); 
134                                         if (!passed)
135                                             return passed;
136                                     }
137                                 }
138                                 return passed;
139                             } else {
140                                 throw new PlistSerdeException("Field " ~ field_names[i] ~ " was not coercible to a " ~ element.type);
141                             }
142                         }
143 
144                         static foreach(etype; [EnumMembers!PlistElementType]) {
145                             if (element.type == etype) { // Implicit that element.type will never be bool here
146                                 static if (typeIsCoercible!field(etype)) {
147                                     enum type = getElementClassFromType(etype);
148                                     mixin (type ~ " val = cast(" ~ type ~ ")element;"); 
149                                     mixin ("obj." ~ field_names[i] ~ " = val.value;");
150                                     return true; // Jump out of the massive branch quicker
151                                 } else {
152                                     throw new PlistSerdeException("Field " ~ field_names[i] ~ " was not coercible to a " ~ element.type);
153                                 }
154                             }
155                         }
156                     }
157                     else {
158                         static if (hasUDA!(F, PlistOptional)) {
159                             return true;
160                         } else {
161                             throw new PlistSerdeException("Missing field " ~ key);
162                         }
163                     }
164                 } else {
165                     return true;
166                 }
167 
168                 // catch-all
169                 return false;
170             }();
171 
172             if (!fail) // Jump out quick, in case one field wasn't able to deserialize, followed by one that WAS
173                 return fail;
174         }
175         return fail;
176     }
177 
178 
179     @property string[] keys() {
180         auto keys = _entries.keys.sort!("a.toLower() < b.toLower()")();
181         return keys.array;
182     }
183 
184     override string toString() {
185         import std.format : format;
186         string ret = format!"<PlistElementDict keys: %s, n_keys: %s>"(_entries.keys, _entries.keys.length);
187         return ret;
188     }
189 
190     void write(ref XMLWriter!(Appender!string) writer) {
191         writer.writeStartTag("dict");
192         // order doesn't matter, so sort it alphabetically
193         foreach (key; keys()) {
194             PlistElement e = _entries[key];
195             writer.openStartTag("key");
196             writer.closeStartTag();
197             writer.writeText(key, Newline.no);
198             writer.writeEndTag("key", Newline.no);
199             e.write(writer);
200         }
201         writer.writeEndTag("dict");
202 
203     }
204 
205     private {
206         DOMEntity!string _entity;
207         PlistElement[string] _entries;
208     }
209 }
210