1 /**
2 Copyright: Copyright (c) 2015-2016 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.config.configmanager;
7 
8 import std.experimental.logger;
9 import std.file : read, exists;
10 public import std.variant;
11 import std.traits : isArray;
12 import std.conv : to;
13 import core.runtime : Runtime;
14 
15 import pluginlib;
16 import sdlang;
17 
18 alias ConfigValue = Algebraic!(int, double, string, bool, int[], double[]);
19 
20 final class ConfigOption
21 {
22 	this(ConfigValue value, ConfigValue defaultValue)
23 	{
24 		this.value = value;
25 		this.defaultValue = defaultValue;
26 	}
27 
28 	T get(T)()
29 	{
30 		static if (isArray!T)
31 			return value.get!T();
32 		else
33 			return value.coerce!T();
34 	}
35 
36 	T set(T)(T newValue)
37 	{
38 		value = newValue;
39 		return newValue;
40 	}
41 
42 	ConfigValue value;
43 	ConfigValue defaultValue;
44 }
45 
46 final class ConfigManager : IResourceManager
47 {
48 private:
49 	ConfigOption[string] options;
50 	string filename;
51 
52 public:
53 
54 	override string id() @property { return "voxelman.managers.configmanager"; }
55 
56 	this(string filename)
57 	{
58 		this.filename = filename;
59 	}
60 
61 	override void loadResources()
62 	{
63 		load();
64 	}
65 
66 	// Runtime options are not saved. Use them to store global options that need no saving
67 	ConfigOption registerOption(T)(string optionName, T defaultValue)
68 	{
69 		if (auto opt = optionName in options)
70 			return *opt;
71 		auto option = new ConfigOption(ConfigValue(defaultValue), ConfigValue(defaultValue));
72 		options[optionName] = option;
73 		return option;
74 	}
75 
76 	ConfigOption opIndex(string optionName)
77 	{
78 		return options.get(optionName, null);
79 	}
80 
81 	void load()
82 	{
83 		bool readConfigFile = exists(filename);
84 		Tag root;
85 		string[] args = Runtime.args;
86 
87 		if (readConfigFile)
88 		try
89 		{
90 			string fileData = cast(string)read(filename);
91 			root = parseSource(fileData, filename);
92 		}
93 		catch(SDLangParseException e)
94 		{
95 			warning(e.msg);
96 			return;
97 		}
98 
99 		auto tempSep = std.getopt.arraySep;
100 		std.getopt.arraySep = ",";
101 		foreach(optionPair; options.byKeyValue)
102 		{
103 			if (readConfigFile)
104 			{
105 				parseValueFromConfig(optionPair.key, optionPair.value, root);
106 			}
107 
108 			// override from console
109 			parseValueFromCmd(optionPair.key, optionPair.value, args);
110 		}
111 		std.getopt.arraySep = tempSep;
112 	}
113 
114 	void save() {}
115 
116 private:
117 
118 	static void parseValueFromConfig(string optionName, ConfigOption option, Tag root)
119 	{
120 		if (optionName !in root.tags) return;
121 		auto tags = root.tags[optionName];
122 
123 		if (tags.length == 1)
124 		{
125 			try
126 			{
127 				parseValue(optionName, option, tags[0].values);
128 				//infof("%s %s %s", optionName, option.value, option.defaultValue);
129 			}
130 			catch(VariantException e)
131 			{
132 				warningf("Error parsing config option: %s - %s", optionName, e.msg);
133 			}
134 		}
135 		else if (tags.length > 1)
136 			warningf("Multiple definitions of '%s'", optionName);
137 		else
138 			warningf("Empty option '%s'", optionName);
139 	}
140 
141 	static void parseValue(string optionName, ConfigOption option, Value[] values)
142 	{
143 		if (values.length == 1)
144 		{
145 			Value value = values[0];
146 
147 			if (option.value.type == typeid(bool)) {
148 				option.value = ConfigValue(value.coerce!bool);
149 			}
150 			else if (option.value.type == typeid(string)) {
151 				option.value = ConfigValue(value.coerce!string);
152 			}
153 			else if (option.value.type == typeid(int)) {
154 				option.value = ConfigValue(value.coerce!int);
155 			}
156 			else if (option.value.type == typeid(double)) {
157 				option.value = ConfigValue(value.coerce!double);
158 			}
159 			else
160 			{
161 				warningf("Cannot parse '%s' from '%s'", optionName, value.to!string);
162 			}
163 		}
164 		else if (values.length > 1)
165 		{
166 			void parseArray(T)()
167 			{
168 				T[] items;
169 				foreach(v; values)
170 					items ~= v.coerce!T;
171 				option.value = ConfigValue(items);
172 			}
173 			if (option.value.length != values.length)
174 				return;
175 
176 			if (option.value.type == typeid(int[])) {
177 				parseArray!int;
178 			} else if (option.value.type == typeid(double[])) {
179 				parseArray!double;
180 			} else {
181 				warningf("Cannot parse '%s' from '%s'", optionName, values.to!string);
182 			}
183 			//infof("conf %s %s", optionName, option.value);
184 		}
185 	}
186 
187 	static void parseValueFromCmd(string optionName, ConfigOption option, string[] args)
188 	{
189 		if (option.value.type == typeid(int)) {
190 			parseSingle!int(optionName, option, args);
191 		}
192 		else if (option.value.type == typeid(double)) {
193 			parseSingle!double(optionName, option, args);
194 		}
195 		else if (option.value.type == typeid(string)) {
196 			parseSingle!string(optionName, option, args);
197 		}
198 		else if (option.value.type == typeid(bool)) {
199 			parseSingle!bool(optionName, option, args);
200 		}
201 		else if (option.value.type == typeid(int[])) {
202 			parseSingle!(int[])(optionName, option, args);
203 		}
204 		else if (option.value.type == typeid(double[])) {
205 			parseSingle!(double[])(optionName, option, args);
206 		}
207 	}
208 }
209 
210 import std.getopt;
211 void parseSingle(T)(string optionName, ConfigOption option, string[] args)
212 {
213 	T val = option.get!T;
214 	//infof("cmd1 %s %s", optionName, val);
215 
216 	static if(is(T == int[]) || is(T == double[]))
217 	{
218 		T newArray;
219 		auto res = getopt(args, std.getopt.config.passThrough, optionName, &newArray);
220 		if (newArray != newArray.init)
221 			val = newArray;
222 	}
223 	else
224 	{
225 		getopt(args, std.getopt.config.passThrough, optionName, &val);
226 	}
227 	//infof("cmd2 %s %s", optionName, val);
228 
229 	option.value = ConfigValue(val);
230 }