PageRenderTime 62ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/src/echessd_config_parser.erl

http://github.com/tuxofil/echessd
Erlang | 320 lines | 258 code | 27 blank | 35 comment | 3 complexity | 801683d4e8c39dffae8ebfc1ea25fd43 MD5 | raw file
Possible License(s): BSD-2-Clause
  1. %%% @doc
  2. %%% Echessd configuration file parser.
  3. %%% @author Aleksey Morarash <aleksey.morarash@gmail.com>
  4. %%% @since 22 Nov 2013
  5. %%% @copyright 2013, Aleksey Morarash
  6. -module(echessd_config_parser).
  7. %% API exports
  8. -export([read/1, default/1]).
  9. -include("echessd.hrl").
  10. %% ----------------------------------------------------------------------
  11. %% Type definitions
  12. %% ----------------------------------------------------------------------
  13. -export_type(
  14. [config/0,
  15. config_item/0,
  16. config_key/0,
  17. loglevel/0
  18. ]).
  19. -type config() :: [config_item()].
  20. -type config_item() ::
  21. {?CFG_LOGLEVEL, Loglevel :: loglevel()} |
  22. {?CFG_LOGFILE, LogFilePath :: nonempty_string() | undefined} |
  23. {?CFG_BIND_ADDR, BindIP :: inet:ip_address()} |
  24. {?CFG_BIND_PORT, BindPort :: inet:port_number()} |
  25. {?CFG_DEF_LANG, DefaultLanguageID :: atom()} |
  26. {?CFG_DEF_STYLE, DefaultStyleID :: atom()} |
  27. {?CFG_XMPP_USER, XmppUsername :: string()} |
  28. {?CFG_XMPP_SERVER, XmppServerHostname :: string()} |
  29. {?CFG_XMPP_PASSWORD, XmppPassword :: string()} |
  30. {?CFG_XMPP_ENABLED, XmppNotificationsEnabled :: boolean()} |
  31. {?CFG_SHOW_ABOUT, ShowAboutInfo :: boolean()} |
  32. {?CFG_SHOW_COPYRIGHTS, ShowCopyrights :: boolean()} |
  33. {?CFG_MIME_TYPES, MimeTypesFilePath :: file:filename()} |
  34. {?CFG_INSTANCE_ID, InstanceID :: atom()} |
  35. {?CFG_COOKIE, Cookie :: atom()} |
  36. {?CFG_DB_PATH, DatabasePath :: nonempty_string()}.
  37. -type config_key() ::
  38. ?CFG_LOGLEVEL | ?CFG_LOGFILE | ?CFG_BIND_ADDR | ?CFG_BIND_PORT |
  39. ?CFG_DEF_LANG | ?CFG_DEF_STYLE | ?CFG_XMPP_USER | ?CFG_XMPP_SERVER |
  40. ?CFG_XMPP_PASSWORD | ?CFG_XMPP_ENABLED | ?CFG_SHOW_ABOUT |
  41. ?CFG_SHOW_COPYRIGHTS | ?CFG_MIME_TYPES | ?CFG_INSTANCE_ID |
  42. ?CFG_COOKIE | ?CFG_DB_PATH.
  43. -type loglevel() :: ?LOG_ERR | ?LOG_INFO | ?LOG_DEBUG.
  44. %% ----------------------------------------------------------------------
  45. %% API functions
  46. %% ----------------------------------------------------------------------
  47. %% @doc Read and parse the configuration file.
  48. -spec read(ConfigPath :: file:filename()) -> config().
  49. read(ConfigPath) ->
  50. add_defaults(parse(read_file(ConfigPath))).
  51. %% @doc Return a default value for the configuration key.
  52. -spec default(Key :: config_key()) -> DefaultValue :: any().
  53. default(?CFG_LOGLEVEL) ->
  54. ?LOG_INFO;
  55. default(?CFG_LOGFILE) ->
  56. undefined;
  57. default(?CFG_BIND_ADDR) ->
  58. {0,0,0,0};
  59. default(?CFG_BIND_PORT) ->
  60. 8888;
  61. default(?CFG_DEF_LANG) ->
  62. en;
  63. default(?CFG_DEF_STYLE) ->
  64. default;
  65. default(?CFG_XMPP_ENABLED) ->
  66. false;
  67. default(?CFG_XMPP_USER) ->
  68. "";
  69. default(?CFG_XMPP_SERVER) ->
  70. "";
  71. default(?CFG_XMPP_PASSWORD) ->
  72. "";
  73. default(?CFG_SHOW_ABOUT) ->
  74. true;
  75. default(?CFG_SHOW_COPYRIGHTS) ->
  76. true;
  77. default(?CFG_MIME_TYPES) ->
  78. "/etc/mime.types";
  79. default(?CFG_INSTANCE_ID) ->
  80. echessd;
  81. default(?CFG_COOKIE) ->
  82. echessd;
  83. default(?CFG_DB_PATH) ->
  84. "./echessd-database/".
  85. %% ----------------------------------------------------------------------
  86. %% Internal functions
  87. %% ----------------------------------------------------------------------
  88. -spec read_file(ConfigPath :: file:filename()) -> string().
  89. read_file(ConfigPath) ->
  90. case file:read_file(ConfigPath) of
  91. {ok, Binary} ->
  92. binary_to_list(Binary);
  93. {error, Reason} ->
  94. echessd_log:err(
  95. "~w> Failed to read configuration file \"~s\": ~9999p",
  96. [?MODULE, ConfigPath, Reason]),
  97. []
  98. end.
  99. %% @doc Parse the contents of the configuration file
  100. %% to a valid key-value pairs.
  101. -spec parse(ConfigContents :: string()) -> config().
  102. parse(ConfigContents) ->
  103. lists:flatmap(fun parse_kv/1, preparse(ConfigContents)).
  104. %% @doc Parse the key-value pair.
  105. -spec parse_kv({LineNo :: pos_integer(),
  106. StrKey :: nonempty_string(),
  107. StrValue :: string()}) ->
  108. [{Key :: config_key(), Value :: any()}].
  109. parse_kv({LineNo, StrKey, StrValue}) ->
  110. case echessd_lib:list_to_atom(StrKey, ?CFGS) of
  111. {ok, Key} ->
  112. case parse_value(Key, StrValue) of
  113. {ok, Value} ->
  114. [{Key, Value}];
  115. error ->
  116. echessd_log:err(
  117. "~w> Bad value for '~w' at line ~w: ~9999p",
  118. [?MODULE, Key, LineNo, StrValue]),
  119. []
  120. end;
  121. error ->
  122. echessd_log:err(
  123. "~w> Unknown configuration key at line ~w: ~9999p",
  124. [?MODULE, LineNo, StrKey]),
  125. []
  126. end.
  127. %% @doc Complete the config with default values for undefined
  128. %% configuration items.
  129. -spec add_defaults(Config :: config()) -> FinalConfig :: config().
  130. add_defaults(Config) ->
  131. [{Key, proplists:get_value(Key, Config, default(Key))} ||
  132. Key <- ?CFGS].
  133. %% @equiv preparse(String, [{comment_chars, "#%!"}])
  134. %% @doc Make preparse of key-value configuration file.
  135. -spec preparse(String :: string()) ->
  136. MeaningLines ::
  137. [{LineNo :: pos_integer(),
  138. Key :: nonempty_string(),
  139. Value :: string()}].
  140. preparse(String) ->
  141. preparse(String, [{comment_chars, "#%!"}]).
  142. %% @doc Make preparse of key-value configuration file.
  143. -spec preparse(String :: string(),
  144. Options ::
  145. [{comment_chars, Chars :: string()}]) ->
  146. MeaningLines ::
  147. [{LineNo :: pos_integer(),
  148. Key :: nonempty_string(),
  149. Value :: string()}].
  150. preparse(String, Options) ->
  151. CommentChars = proplists:get_value(comment_chars, Options, ""),
  152. {_LastLineNo, CommentChars, List} =
  153. lists:foldl(
  154. fun preparse_loop/2,
  155. {1, CommentChars, []},
  156. split_lines(String)),
  157. lists:reverse(List).
  158. -type preparse_acc() ::
  159. {LineNo :: pos_integer(),
  160. CommentChars :: string(),
  161. ParsedLinesAccumulator :: [{LineNo :: pos_integer(),
  162. Key :: nonempty_string(),
  163. Value :: string()}]}.
  164. %% @doc
  165. -spec preparse_loop(Line :: string(), Acc :: preparse_acc()) ->
  166. NewAcc :: preparse_acc().
  167. preparse_loop(Line, {LineNo, CommentChars, Parsed}) ->
  168. case echessd_lib:strip(Line, " \t\r\n") of
  169. [] ->
  170. {LineNo + 1, CommentChars, Parsed};
  171. [C | _] = Stripped ->
  172. case lists:member(C, CommentChars) of
  173. true ->
  174. {LineNo + 1, CommentChars, Parsed};
  175. _ ->
  176. case split_to_key_value(Stripped) of
  177. {[_ | _] = Key, Value} ->
  178. {LineNo + 1, CommentChars,
  179. [{LineNo, Key, Value} | Parsed]};
  180. _ ->
  181. {LineNo + 1, CommentChars, Parsed}
  182. end
  183. end
  184. end.
  185. %% @doc Split the text to lines. Result lines will be stripped
  186. %% of newline characters.
  187. -spec split_lines(String :: string()) -> Lines :: [string()].
  188. split_lines(String) ->
  189. split_lines(String, [], []).
  190. split_lines([], [], Lines) ->
  191. lists:reverse(Lines);
  192. split_lines([], Line, Lines) ->
  193. split_lines([], [], [lists:reverse(Line) | Lines]);
  194. split_lines("\r\n" ++ Tail, Line, Lines) ->
  195. split_lines(Tail, [], [lists:reverse(Line) | Lines]);
  196. split_lines("\n" ++ Tail, Line, Lines) ->
  197. split_lines(Tail, [], [lists:reverse(Line) | Lines]);
  198. split_lines("\r" ++ Tail, Line, Lines) ->
  199. split_lines(Tail, Line, Lines);
  200. split_lines([C | Tail], Line, Lines) ->
  201. split_lines(Tail, [C | Line], Lines).
  202. %% @doc Split the line to a key and a value. The first token separated
  203. %% from the others with space characters, will be returned as the key in
  204. %% lower case (so, keys are case insensitive); rest of the line will be
  205. %% returned as the value. Character case of the value will be preserved.
  206. -spec split_to_key_value(String :: string()) ->
  207. {Key :: string(), Value :: string()}.
  208. split_to_key_value(String) ->
  209. split_to_key_value_(echessd_lib:strip(String, " \t\r\n"), []).
  210. split_to_key_value_([], Key) ->
  211. {string:to_lower(lists:reverse(Key)), []};
  212. split_to_key_value_([C | Tail], Key) ->
  213. case lists:member(C, " \t") of
  214. true ->
  215. {string:to_lower(lists:reverse(Key)),
  216. echessd_lib:strip(Tail, " \t")};
  217. _ ->
  218. split_to_key_value_(Tail, [C | Key])
  219. end.
  220. %% @doc Parse the value for the given configuration key.
  221. -spec parse_value(Key :: config_key(), String :: string()) ->
  222. {ok, Value :: any()} | error.
  223. parse_value(Key, StrValue) ->
  224. try
  225. {ok, parse_value_(Key, StrValue)}
  226. catch
  227. _:_ ->
  228. error
  229. end.
  230. %% @doc
  231. -spec parse_value_(Key :: config_key(), StrValue :: string()) ->
  232. ParsedValue :: any().
  233. parse_value_(?CFG_LOGLEVEL, String) ->
  234. {ok, Loglevel} = echessd_lib:list_to_atom(String, ?LOG_LEVELS),
  235. Loglevel;
  236. parse_value_(?CFG_LOGFILE, [_ | _] = String) ->
  237. String;
  238. parse_value_(?CFG_LOGFILE, []) ->
  239. undefined;
  240. parse_value_(?CFG_BIND_ADDR, String) ->
  241. {ok, IP} = inet_parse:address(String),
  242. IP;
  243. parse_value_(?CFG_BIND_PORT, String) ->
  244. Int = list_to_integer(String),
  245. true = Int > 0 andalso Int =< 16#ffff,
  246. Int;
  247. parse_value_(?CFG_DEF_LANG, String) ->
  248. Langs = [LangID || {LangID, _LangName} <- echessd_lang:list()],
  249. {ok, LangID} = echessd_lib:list_to_atom(String, Langs),
  250. LangID;
  251. parse_value_(?CFG_DEF_STYLE, String) ->
  252. Styles = [StyleID || {StyleID, _TextID} <- echessd_styles:list()],
  253. {ok, StyleID} = echessd_lib:list_to_atom(String, Styles),
  254. StyleID;
  255. parse_value_(?CFG_XMPP_ENABLED, String) ->
  256. {ok, Boolean} = parse_boolean(String),
  257. Boolean;
  258. parse_value_(?CFG_XMPP_USER, String) ->
  259. String;
  260. parse_value_(?CFG_XMPP_SERVER, String) ->
  261. String;
  262. parse_value_(?CFG_XMPP_PASSWORD, String) ->
  263. String;
  264. parse_value_(?CFG_SHOW_ABOUT, String) ->
  265. {ok, Boolean} = parse_boolean(String),
  266. Boolean;
  267. parse_value_(?CFG_SHOW_COPYRIGHTS, String) ->
  268. {ok, Boolean} = parse_boolean(String),
  269. Boolean;
  270. parse_value_(?CFG_MIME_TYPES, String) ->
  271. String;
  272. parse_value_(?CFG_INSTANCE_ID, String) ->
  273. list_to_atom(String);
  274. parse_value_(?CFG_COOKIE, String) ->
  275. list_to_atom(String);
  276. parse_value_(?CFG_DB_PATH, String) ->
  277. String.
  278. %% @doc
  279. -spec parse_boolean(String :: string()) -> {ok, boolean()} | error.
  280. parse_boolean(String) ->
  281. Lowered = string:to_lower(String),
  282. case lists:member(Lowered, ["y", "yes", "t", "true", "1", "on"]) of
  283. true ->
  284. {ok, true};
  285. _ ->
  286. case lists:member(Lowered,
  287. ["n", "no", "f", "false", "0", "off"]) of
  288. true ->
  289. {ok, false};
  290. _ ->
  291. error
  292. end
  293. end.