1 /*
2  * Copyright (c) 2017-2018 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: 2017-2018 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/selery/source/selery/node/plugin/config.d, selery/node/plugin/config.d)
28  */
29 module selery.node.plugin.config;
30 
31 import std.ascii : newline;
32 import std.conv : to, ConvException;
33 import std.string : split, join, strip, indexOf;
34 import std.traits : isArray;
35 
36 import selery.node.plugin.file;
37 
38 enum _;
39 
40 struct Value(T) {
41 
42 	alias Type = T;
43 
44 	string name;
45 	T def;
46 
47 }
48 
49 Value!T value(T)(string name, T def) {
50 	return Value!T(name, def);
51 }
52 
53 /**
54  * Container for configuration files with compile-time format
55  * and members.
56  * 
57  * The file is saved in a text file in format "field: value",
58  * one per line, with every value converted into a string.
59  * Supported values are the basic numeric types, string and
60  * arrays of these two.
61  * 
62  * Use $(D value) to indicate a field with a static type, $(D _) to
63  * insert an empty line and a string to insert a comment.
64  * 
65  * Example:
66  * ---
67  * Config!(Value!string("name"), _, Value!uint("players", 0), Value!uint("max", 256), _, Value!(uint[])("array")) example;
68  * assert(example.players == 0);
69  * assert(example.max == 256);
70  * 
71  * alias Example = Config!("comment", value("field", "value"), _, value("int", 12u));
72  * Example.init.save("example.txt");
73  * assert(read("example.txt") == "# comment\nfield: value\n\nint: 12\n"); // with posix's line-endings
74  * ---
75  */
76 struct Config(E...) if(areValidArgs!E) {
77 
78 	private string file;
79 
80 	mixin((){
81 		string ret;
82 		foreach(immutable i, T; E) {
83 			static if(!is(T == _) && !is(typeof(T) : string)) {
84 				ret ~= "E[" ~ to!string(i) ~ "].Type " ~ T.name ~ "=E[" ~ to!string(i) ~ "].def;";
85 			}
86 		}
87 		return ret;
88 	}());
89 
90 	/**
91 	 * Loads the values from a file.
92 	 * Throws:
93 	 * 		ConvException if one of the value cannot be converted from string to the requested one
94 	 * 		FileException on file error
95 	 */
96 	public void load(string sep=":", string mod=__MODULE__)(string file) {
97 		if(exists!mod(file)) {
98 			string[] lines = (cast(string)read!mod(file)).split("\n");
99 			foreach(string line ; lines) {
100 				if(line.length && line[0] != '#') {
101 					auto index = line.indexOf(sep);
102 					if(index > 0) {
103 						string name = line[0..index].strip;
104 						string value = line[index+1..$].strip;
105 						foreach(immutable i, T; E) {
106 							static if(!is(T == _) && !is(typeof(T) : string)) {
107 								if(name == T.name) {
108 									static if(!isArray!(T.Type) || is(T.Type : string)) {
109 										try {
110 											mixin("this." ~ T.name ~ " = to!(T.Type)(value);");
111 										} catch(ConvException) {}
112 									} else {
113 										foreach(el ; value.split(",")) {
114 											try {
115 												mixin("this." ~ T.name ~ " ~= to!(typeof(T.Type.init[0]))(el.strip);");
116 											} catch(ConvException) {}
117 										}
118 									}
119 								}
120 							}
121 						}
122 					}
123 				}
124 			}
125 		}
126 		this.file = file;
127 	}
128 
129 	/**
130 	 * Saves the field's values into a file. If none is given
131 	 * the values are saved in the same file they have been
132 	 * loaded from (if the load method has been called), otherwise
133 	 * the file is not saved.
134 	 * Throws:
135 	 * 		FileException on file error
136 	 * Example:
137 	 * ---
138 	 * example.save("example.txt");
139 	 * example.save("dir/test.txt");
140 	 * assert(read("example.txt") == read("dir/test.txt"));
141 	 * ---
142 	 */
143 	public void save(string sep=":", string mod=__MODULE__)(string file) {
144 		string data;
145 		foreach(immutable i, T; E) {
146 			static if(is(T == _)) {
147 				data ~= newline;
148 			} else static if(is(typeof(T) : string)) {
149 				data ~= "# " ~ T ~ newline;
150 			} else {
151 				data ~= T.name ~ sep ~ " ";
152 				static if(!isArray!(T.Type) || is(T.Type : string)) {
153 					mixin("data ~= to!string(this." ~ T.name ~ ");");
154 				} else {
155 					mixin("auto array = this." ~ T.name ~ ";");
156 					string[] d;
157 					foreach(a ; array) d ~= to!string(a);
158 					data ~= d.join(", ");
159 				}
160 				data ~= newline;
161 			}
162 		}
163 		write!mod(file, data);
164 	}
165 
166 	/// ditto
167 	public void save(string sep=":", string mod=__MODULE__)() {
168 		if(this.file.length) this.save!(sep, mod)(this.file);
169 	}
170 
171 }
172 
173 private bool areValidArgs(E...)() {
174 	foreach(T ; E) {
175 		static if(!is(T == _) && !is(typeof(T) : string) && !is(T == struct) && !is(typeof(T.name) == string) && !is(typeof(T.def) == T.Type)) return false;
176 	}
177 	return true;
178 }