Newer
Older
dub_jkp / source / dub / internal / configy / Attributes.d
@Mathias Lang Mathias Lang on 12 Jan 2024 10 KB Configy: Synchronize with upstream
  1. /*******************************************************************************
  2.  
  3. Define UDAs that can be applied to a configuration struct
  4.  
  5. This module is stand alone (a leaf module) to allow importing the UDAs
  6. without importing the whole configuration parsing code.
  7.  
  8. Copyright:
  9. Copyright (c) 2019-2022 BOSAGORA Foundation
  10. All rights reserved.
  11.  
  12. License:
  13. MIT License. See LICENSE for details.
  14.  
  15. *******************************************************************************/
  16.  
  17. module dub.internal.configy.Attributes;
  18.  
  19. import std.traits;
  20.  
  21. /*******************************************************************************
  22.  
  23. An optional parameter with an initial value of `T.init`
  24.  
  25. The config parser automatically recognize non-default initializer,
  26. so that the following:
  27. ```
  28. public struct Config
  29. {
  30. public string greeting = "Welcome home";
  31. }
  32. ```
  33. Will not error out if `greeting` is not defined in the config file.
  34. However, this relies on the initializer of the field (`greeting`) being
  35. different from the type initializer (`string.init` is `null`).
  36. In some cases, the default value is also the desired initializer, e.g.:
  37. ```
  38. public struct Config
  39. {
  40. /// Maximum number of connections. 0 means unlimited.
  41. public uint connections_limit = 0;
  42. }
  43. ```
  44. In this case, one can add `@Optional` to the field to inform the parser.
  45.  
  46. *******************************************************************************/
  47.  
  48. public struct Optional {}
  49.  
  50. /*******************************************************************************
  51.  
  52. Inform the config filler that this sequence is to be read as a mapping
  53.  
  54. On some occasions, one might want to read a mapping as an array.
  55. One reason to do so may be to provide a better experience to the user,
  56. e.g. having to type:
  57. ```
  58. interfaces:
  59. eth0:
  60. ip: "192.168.0.1"
  61. private: true
  62. wlan0:
  63. ip: "1.2.3.4"
  64. ```
  65. Instead of the slightly more verbose:
  66. ```
  67. interfaces:
  68. - name: eth0
  69. ip: "192.168.0.1"
  70. private: true
  71. - name: wlan0
  72. ip: "1.2.3.4"
  73. ```
  74.  
  75. The former would require to be expressed as an associative arrays.
  76. However, one major drawback of associative arrays is that they can't have
  77. an initializer, which makes them cumbersome to use in the context of the
  78. config filler. To remediate this issue, one may use `@Key("name")`
  79. on a field (here, `interfaces`) so that the mapping is flattened
  80. to an array. If `name` is `null`, the key will be discarded.
  81.  
  82. *******************************************************************************/
  83.  
  84. public struct Key
  85. {
  86. ///
  87. public string name;
  88. }
  89.  
  90. /*******************************************************************************
  91.  
  92. Look up the provided name in the YAML node, instead of the field name.
  93.  
  94. By default, the config filler will look up the field name of a mapping in
  95. the YAML node. If this is not desired, an explicit `Name` attribute can
  96. be given. This is especially useful for names which are keyword.
  97.  
  98. ```
  99. public struct Config
  100. {
  101. public @Name("delete") bool remove;
  102. }
  103. ```
  104.  
  105. *******************************************************************************/
  106.  
  107. public struct Name
  108. {
  109. ///
  110. public string name;
  111.  
  112. ///
  113. public bool startsWith;
  114. }
  115.  
  116. /// Short hand syntax
  117. public Name StartsWith(string name) @safe pure nothrow @nogc
  118. {
  119. return Name(name, true);
  120. }
  121.  
  122. /*******************************************************************************
  123.  
  124. A field which carries information about whether it was set or not
  125.  
  126. Some configurations may need to know which fields were set explicitly while
  127. keeping defaults. An example of this is a `struct` where at least one field
  128. needs to be set, such as the following:
  129. ```
  130. public struct ProtoDuration
  131. {
  132. public @Optional long weeks;
  133. public @Optional long days;
  134. public @Optional long hours;
  135. public @Optional long minutes;
  136. public long seconds = 42;
  137. public @Optional long msecs;
  138. public @Optional long usecs;
  139. public @Optional long hnsecs;
  140. public @Optional long nsecs;
  141. }
  142. ```
  143. In this case, it would be impossible to know if any field was explicitly
  144. provided. Hence, the struct should be written as:
  145. ```
  146. public struct ProtoDuration
  147. {
  148. public SetInfo!long weeks;
  149. public SetInfo!long days;
  150. public SetInfo!long hours;
  151. public SetInfo!long minutes;
  152. public SetInfo!long seconds = 42;
  153. public SetInfo!long msecs;
  154. public SetInfo!long usecs;
  155. public SetInfo!long hnsecs;
  156. public SetInfo!long nsecs;
  157. }
  158. ```
  159. Note that `SetInfo` implies `Optional`, and supports default values.
  160.  
  161. *******************************************************************************/
  162.  
  163. public struct SetInfo (T)
  164. {
  165. /***************************************************************************
  166.  
  167. Allow initialization as a field
  168.  
  169. This sets the field as having been set, so that:
  170. ```
  171. struct Config { SetInfo!Duration timeout; }
  172.  
  173. Config myConf = { timeout: 10.minutes }
  174. ```
  175. Will behave as if set explicitly. If this behavior is not wanted,
  176. pass `false` as second argument:
  177. ```
  178. Config myConf = { timeout: SetInfo!Duration(10.minutes, false) }
  179. ```
  180.  
  181. ***************************************************************************/
  182.  
  183. public this (T initVal, bool isSet = true) @safe pure nothrow @nogc
  184. {
  185. this.value = initVal;
  186. this.set = isSet;
  187. }
  188.  
  189. /// Underlying data
  190. public T value;
  191.  
  192. ///
  193. alias value this;
  194.  
  195. /// Whether this field was set or not
  196. public bool set;
  197. }
  198.  
  199. /*******************************************************************************
  200.  
  201. Provides a means to convert a field from a `Node` to a complex type
  202.  
  203. When filling the config, it might be useful to store types which are
  204. not only simple `string` and integer, such as `URL`, `BigInt`, or any other
  205. library type not directly under the user's control.
  206.  
  207. To allow reading those values from the config file, a `Converter` may
  208. be used. The converter will tell the `ConfigFiller` how to convert from
  209. `Node` to the desired type `T`.
  210.  
  211. If the type is under the user's control, one can also add a constructor
  212. accepting a single string, or define the `fromString` method, both of which
  213. are tried if no `Converter` is found.
  214.  
  215. For types not under the user's control, there might be different ways
  216. to parse the same type within the same struct, or neither the ctor nor
  217. the `fromString` method may be defined under that name.
  218. The exmaple below uses `parse` in place of `fromString`, for example.
  219.  
  220. ```
  221. /// Complex structure representing the age of a person based on its birthday
  222. public struct Age
  223. {
  224. ///
  225. public uint birth_year;
  226. ///
  227. public uint birth_month;
  228. ///
  229. public uint birth_day;
  230.  
  231. /// Note that this will be picked up automatically if named `fromString`
  232. /// but this struct might be a library type.
  233. public static Age parse (string value) { /+ Magic +/ }
  234. }
  235.  
  236. public struct Person
  237. {
  238. ///
  239. @Converter!Age((Node value) => Age.parse(value.as!string))
  240. public Age age;
  241. }
  242. ```
  243.  
  244. Note that some fields may also be of multiple YAML types, such as DUB's
  245. `dependencies`, which is either a simple string (`"vibe-d": "~>1.0 "`),
  246. or an in its complex form (`"vibe-d": { "version": "~>1.0" }`).
  247. For those use cases, a `Converter` is the best approach.
  248.  
  249. To avoid repeating the field type, a convenience function is provided:
  250. ```
  251. public struct Age
  252. {
  253. public uint birth_year;
  254. public uint birth_month;
  255. public uint birth_day;
  256. public static Age parse (string value) { /+ Magic +/ }
  257. }
  258.  
  259. public struct Person
  260. {
  261. /// Here `converter` will deduct the type from the delegate argument,
  262. /// and return an instance of `Converter`. Mind the case.
  263. @converter((Node value) => Age.parse(value.as!string))
  264. public Age age;
  265. }
  266. ```
  267.  
  268. *******************************************************************************/
  269.  
  270. public struct Converter (T)
  271. {
  272. ///
  273. public alias ConverterFunc = T function (scope ConfigParser!T context);
  274.  
  275. ///
  276. public ConverterFunc converter;
  277. }
  278.  
  279. /// Ditto
  280. public auto converter (FT) (FT func)
  281. {
  282. static assert(isFunctionPointer!FT,
  283. "Error: Argument to `converter` should be a function pointer, not: "
  284. ~ FT.stringof);
  285.  
  286. alias RType = ReturnType!FT;
  287. static assert(!is(RType == void),
  288. "Error: Converter needs to be of the return type of the field, not `void`");
  289. return Converter!RType(func);
  290. }
  291.  
  292. /*******************************************************************************
  293.  
  294. Interface that is passed to `fromYAML` hook
  295.  
  296. The `ConfigParser` exposes the raw YAML node (`see `node` method),
  297. the path within the file (`path` method), and a simple ability to recurse
  298. via `parseAs`.
  299.  
  300. Params:
  301. T = The type of the structure which defines a `fromYAML` hook
  302.  
  303. *******************************************************************************/
  304.  
  305. public interface ConfigParser (T)
  306. {
  307. import dub.internal.dyaml.node;
  308. import dub.internal.configy.FieldRef : StructFieldRef;
  309. import dub.internal.configy.Read : Context, parseField;
  310.  
  311. /// Returns: the node being processed
  312. public inout(Node) node () inout @safe pure nothrow @nogc;
  313.  
  314. /// Returns: current location we are parsing
  315. public string path () const @safe pure nothrow @nogc;
  316.  
  317. /***************************************************************************
  318.  
  319. Parse this struct as another type
  320.  
  321. This allows implementing union-like behavior, where a `struct` which
  322. implements `fromYAML` can parse a simple representation as one type,
  323. and one more advanced as another type.
  324.  
  325. Params:
  326. OtherType = The type to parse as
  327. defaultValue = The instance to use as a default value for fields
  328.  
  329. ***************************************************************************/
  330.  
  331. public final auto parseAs (OtherType)
  332. (auto ref OtherType defaultValue = OtherType.init)
  333. {
  334. alias TypeFieldRef = StructFieldRef!OtherType;
  335. return this.node().parseField!(TypeFieldRef)(
  336. this.path(), defaultValue, this.context());
  337. }
  338.  
  339. /// Internal use only
  340. protected const(Context) context () const @safe pure nothrow @nogc;
  341. }