1 module siryul.dyaml;
2 import dyaml;
3 import siryul.common;
4 import std.range.primitives : ElementType, isInfinite, isInputRange;
5 import std.traits : isSomeChar;
6 import std.typecons;
7 
8 /++
9  + YAML (YAML Ain't Markup Language) serialization format
10  +/
11 struct YAML {
12 	private import std.meta : AliasSeq;
13 	package alias types = AliasSeq!(".yml", ".yaml");
14 	package enum emptyObject = "---\n...";
15 	package static T parseInput(T, DeSiryulize flags, U)(U data) if (isInputRange!U && isSomeChar!(ElementType!U)) {
16 		import std.conv : to;
17 		import std.utf : byChar;
18 		auto str = data.byChar.to!(char[]);
19 		auto loader = Loader.fromString(str).load();
20 		return loader.fromYAML!(T, BitFlags!DeSiryulize(flags));
21 	}
22 	package static string asString(Siryulize flags, T)(T data) {
23 		import std.array : appender;
24 		debug enum path = T.stringof;
25 		else enum path = "";
26 		auto buf = appender!string;
27 		auto dumper = dumper(buf);
28 		auto representer = new Representer;
29 		representer.defaultCollectionStyle = CollectionStyle.block;
30 		representer.defaultScalarStyle = ScalarStyle.plain;
31 		dumper.representer = representer;
32 		dumper.explicitStart = false;
33 		dumper.dump(toYAML!(BitFlags!Siryulize(flags))(data, path));
34 		return buf.data;
35 	}
36 }
37 /++
38  + Thrown on YAML deserialization errors
39  +/
40 class YAMLDException : DeserializeException {
41 	this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow {
42 		super(msg, file, line);
43 	}
44 }
45 /++
46  + Thrown on YAML serialization errors
47  +/
48 class YAMLSException : SerializeException {
49 	this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow {
50 		super(msg, file, line);
51 	}
52 }
53 private T fromYAML(T, BitFlags!DeSiryulize flags)(Node node) if (!isInfinite!T) {
54 	import std.conv : to;
55 	import std.datetime : Date, DateTime, SysTime, TimeOfDay;
56 	import std.exception : enforce;
57 	import std.meta : AliasSeq;
58 	import std.range : enumerate, isOutputRange;
59 	import std.traits : arity, FieldNameTuple, ForeachType, getUDAs, hasIndirections, hasUDA, isArray, isAssociativeArray, isFloatingPoint, isIntegral, isPointer, isSomeString, isStaticArray, KeyType, OriginalType, Parameters, PointerTarget, TemplateArgsOf, ValueType;
60 	import std.utf : byCodeUnit;
61 	if (node.isNull)
62 		return T.init;
63 	try {
64 		static if (is(T == struct) && hasDeserializationMethod!T) {
65 			return deserializationMethod!T(fromYAML!(Parameters!(deserializationMethod!T), flags)(node));
66 		} else static if (is(T == enum)) {
67 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
68 			if (node.tag == `tag:yaml.org,2002:str`)
69 				return node.get!string.to!T;
70 			else
71 				return node.fromYAML!(OriginalType!T, flags).to!T;
72 		} else static if (isNullable!T) {
73 			return node.isNull ? T.init : T(node.fromYAML!(TemplateArgsOf!T[0], flags));
74 		} else static if (isIntegral!T || isSomeString!T || isFloatingPoint!T) {
75 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
76 			if (node.tag == `tag:yaml.org,2002:str`)
77 				return node.get!string.to!T;
78 			static if (isIntegral!T) {
79 				enforce!YAMLDException(node.tag == `tag:yaml.org,2002:int`, "Attempted to read a float as an integer");
80 				return node.get!T;
81 			} else static if (isSomeString!T) {
82 				enforce!YAMLDException(node.tag != `tag:yaml.org,2002:bool`, "Attempted to read a non-string as a string");
83 				return node.get!string.to!T;
84 			} else {
85 				return node.get!T;
86 			}
87 		} else static if (isSomeChar!T) {
88 			import std.array : front;
89 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
90 			return cast(T)node.get!string.front;
91 		} else static if (is(T == SysTime) || is(T == DateTime) || is(T == Date)) {
92 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
93 			return node.get!SysTime.to!T;
94 		} else static if (is(T == TimeOfDay)) {
95 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
96 			return TimeOfDay.fromISOExtString(node.get!string);
97 		} else static if (is(T == struct)  || (isPointer!T && is(PointerTarget!T == struct))) {
98 			enforce!YAMLDException(node.isMapping(), "Attempted to read a non-mapping as a "~T.stringof);
99 			T output;
100 			static if (isPointer!T) {
101 				output = new PointerTarget!T;
102 				alias Undecorated = PointerTarget!T;
103 			} else {
104 				alias Undecorated = T;
105 			}
106 			foreach (member; FieldNameTuple!Undecorated) {
107 				static if (__traits(getProtection, __traits(getMember, Undecorated, member)) == "public") {
108 					alias field = AliasSeq!(__traits(getMember, Undecorated, member));
109 					enum memberName = getMemberName!field;
110 					static if ((hasUDA!(field, Optional) || (!!(flags & DeSiryulize.optionalByDefault))) || hasIndirections!(typeof(field))) {
111 						if (!node.containsKey(memberName))
112 							continue;
113 					} else {
114 						enforce!YAMLDException(node.containsKey(memberName), "Missing non-@Optional "~memberName~" in node");
115 					}
116 					alias fromFunc = getConvertFromFunc!(T, __traits(getMember, Undecorated, member));
117 					static if (hasUDA!(__traits(getMember, Undecorated, member), IgnoreErrors)) {
118 						try {
119 							__traits(getMember, output, member) = fromFunc(node[memberName].fromYAML!(Parameters!(fromFunc)[0], flags));
120 						} catch (YAMLDException) {}
121 					} else
122 						__traits(getMember, output, member) = fromFunc(node[memberName].fromYAML!(Parameters!(fromFunc)[0], flags));
123 				}
124 			}
125 			return output;
126 		} else static if(isOutputRange!(T, ElementType!T)) {
127 			enforce!YAMLDException(node.isSequence(), "Attempted to read a non-sequence as a "~T.stringof);
128 			T output;
129 			foreach (Node newNode; node)
130 				output ~= fromYAML!(ElementType!T, flags)(newNode);
131 			return output;
132 		} else static if (isStaticArray!T && isSomeChar!(ElementType!T)) {
133 			enforce!YAMLDException(node.isScalar(), "Attempted to read a non-scalar as a "~T.stringof);
134 			T output;
135 			foreach (i, chr; node.fromYAML!((ForeachType!T)[], flags).byCodeUnit.enumerate(0))
136 				output[i] = chr;
137 			return output;
138 		} else static if(isStaticArray!T) {
139 			enforce!YAMLDException(node.isSequence(), "Attempted to read a non-sequence as a "~T.stringof);
140 			T output;
141 			size_t i;
142 			foreach (Node newNode; node)
143 				output[i++] = fromYAML!(ElementType!T, flags)(newNode);
144 			return output;
145 		} else static if(isAssociativeArray!T) {
146 			enforce!YAMLDException(node.isMapping(), "Attempted to read a non-mapping as a "~T.stringof);
147 			T output;
148 			foreach (Node key, Node value; node)
149 				output[fromYAML!(KeyType!T, flags)(key)] = fromYAML!(ValueType!T, flags)(value);
150 			return output;
151 		} else static if (is(T == bool)) {
152 			enforce!YAMLDException(node.tag == `tag:yaml.org,2002:bool`, "Expecting a boolean value");
153 			return node.get!T;
154 		} else
155 			static assert(false, "Cannot read type "~T.stringof~" from YAML"); //unreachable, hopefully.
156 	} catch (NodeException e) {
157 		throw new YAMLDException(e.msg);
158 	}
159 }
160 private @property Node toYAML(BitFlags!Siryulize flags, T)(T type, string path = "") if (!isInfinite!T) {
161 	import std.conv : text, to;
162 	import std.datetime : Date, DateTime, SysTime, TimeOfDay;
163 	import std.meta : AliasSeq;
164 	import std.traits : arity, FieldNameTuple, getSymbolsByUDA, getUDAs, hasUDA, isAssociativeArray, isPointer, isSomeString, isStaticArray, PointerTarget, Unqual;
165 	static if (isPointer!T) {
166 		alias Undecorated = Unqual!(PointerTarget!T);
167 	} else {
168 		alias Undecorated = Unqual!T;
169 	}
170 	static if (is(T == struct) && hasSerializationMethod!T) {
171 		return toYAML!flags(mixin("type."~__traits(identifier, serializationMethod!T)), path);
172 	} else static if (hasUDA!(type, AsString) || is(Undecorated == enum)) {
173 		return Node(type.text);
174 	} else static if (isNullable!Undecorated) {
175 		if (type.isNull) {
176 			return Node(YAMLNull());
177 		} else {
178 			return toYAML!flags(type.get(), path);
179 		}
180 	} else static if (is(Undecorated == SysTime)) {
181 		return Node(type.to!Undecorated, "tag:yaml.org,2002:timestamp");
182 	} else static if (is(Undecorated == DateTime) || is(Undecorated == Date)) {
183 		return Node(type.toISOExtString(), "tag:yaml.org,2002:timestamp");
184 	} else static if (is(Undecorated == TimeOfDay)) {
185 		return Node(type.toISOExtString());
186 	} else static if (isSomeChar!Undecorated) {
187 		return toYAML!flags([type], path);
188 	} else static if (canStoreUnchanged!Undecorated) {
189 		return Node(type.to!Undecorated);
190 	} else static if (isSomeString!Undecorated || (isStaticArray!Undecorated && isSomeChar!(ElementType!Undecorated))) {
191 		import std.utf : toUTF8;
192 		return toYAML!flags(type[].toUTF8().idup, path);
193 	} else static if(isAssociativeArray!Undecorated) {
194 		Node[Node] output;
195 		foreach (key, value; type)
196 			output[toYAML!flags(key, path)] = toYAML!flags(value, path);
197 		return Node(output);
198 	} else static if(isSimpleList!Undecorated) {
199 		Node[] output;
200 		foreach (value; type)
201 			output ~= toYAML!flags(value, path);
202 		return Node(output);
203 	} else static if (is(Undecorated == struct)) {
204 		static string[] empty;
205 		Node output = Node(empty, empty);
206 		foreach (member; FieldNameTuple!Undecorated) {
207 			static if (__traits(getProtection, __traits(getMember, Undecorated, member)) == "public") {
208 				debug string newPath = path~"."~member;
209 				else string newPath = "";
210 				static if (!!(flags & Siryulize.omitInits)) {
211 					static if (isNullable!(typeof(__traits(getMember, type, member)))) {
212 						if (__traits(getMember, type, member).isNull)
213 							continue;
214 					} else {
215 						if (__traits(getMember, type, member) == __traits(getMember, type, member).init) {
216 							continue;
217 						}
218 					}
219 				}
220 				enum memberName = getMemberName!(__traits(getMember, Undecorated, member));
221 				try {
222 					static if (isPointer!(typeof(mixin("type."~member))) && !!(flags & Siryulize.omitNulls)) {
223 						if (mixin("type."~member) is null) {
224 							continue;
225 						}
226 					}
227 					static if (hasConvertToFunc!(T, __traits(getMember, Undecorated, member))) {
228 						auto val = toYAML!flags(getConvertToFunc!(T, __traits(getMember, Undecorated, member))(mixin("type."~member)), newPath);
229 						output.add(memberName, val);
230 					} else {
231 						output.add(memberName, toYAML!(flags)(mixin("type."~member), newPath));
232 					}
233 				} catch (Exception e) {
234 					e.msg = "Error serializing "~newPath~": "~e.msg;
235 					throw e;
236 				}
237 			}
238 		}
239 		return output;
240 	} else
241 		static assert(false, "Cannot write type "~T.stringof~" to YAML"); //unreachable, hopefully
242 }
243 private template canStoreUnchanged(T) {
244 	import std.traits : isFloatingPoint, isIntegral;
245 	enum canStoreUnchanged = isIntegral!T || is(T == bool) || isFloatingPoint!T || is(T == string);
246 }