1 /**
2 Copyright: Copyright (c) 2015-2018 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 voxelman.log;
9 import std.file : read, exists;
10 public import std.variant;
11 import std.traits : isArray;
12 import std.conv : to;
13 
14 import pluginlib;
15 import sdlang;
16 
17 alias ConfigValue = Algebraic!(int, double, string, bool, int[], double[]);
18 
19 final class ConfigOption
20 {
21 	this(ConfigValue value, ConfigValue defaultValue)
22 	{
23 		this.value = value;
24 		this.defaultValue = defaultValue;
25 	}
26 
27 	T get(T)()
28 	{
29 		static if (isArray!T)
30 			return value.get!T();
31 		else
32 			return value.coerce!T();
33 	}
34 
35 	T set(T)(T newValue)
36 	{
37 		value = newValue;
38 		return newValue;
39 	}
40 
41 	ConfigValue value;
42 	ConfigValue defaultValue;
43 }
44 
45 final class ConfigManager : IResourceManager
46 {
47 private:
48 	ConfigOption[string] options;
49 	string filename;
50 	string[] args;
51 
52 public:
53 
54 	override string id() @property { return "voxelman.managers.configmanager"; }
55 
56 	this(string filename, string[] args)
57 	{
58 		this.filename = filename;
59 		this.args = args;
60 	}
61 
62 	override void loadResources()
63 	{
64 		load();
65 	}
66 
67 	// Runtime options are not saved. Use them to store global options that need no saving
68 	ConfigOption registerOption(T)(string optionName, T defaultValue)
69 	{
70 		if (auto opt = optionName in options)
71 			return *opt;
72 		auto option = new ConfigOption(ConfigValue(defaultValue), ConfigValue(defaultValue));
73 		options[optionName] = option;
74 		return option;
75 	}
76 
77 	ConfigOption opIndex(string optionName)
78 	{
79 		return options.get(optionName, null);
80 	}
81 
82 	void load()
83 	{
84 		bool readConfigFile = exists(filename);
85 		Tag root;
86 
87 		if (readConfigFile)
88 		try
89 		{
90 			string fileData = cast(string)read(filename);
91 			root = parseSource(fileData, filename);
92 		}
93 		catch(ParseException 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 }