1 /// Contains services that are used to easily load, modify, and store the program's configuration. 2 module jaster.cli.config; 3 4 private 5 { 6 import std.typecons : Flag; 7 import std.traits : isCopyable; 8 import jaster.ioc; 9 } 10 11 alias WasExceptionThrown = Flag!"wasAnExceptionThrown?"; 12 alias SaveOnSuccess = Flag!"configSaveOnSuccess"; 13 alias RollbackOnFailure = Flag!"configRollbackOnError"; 14 15 /++ 16 + The simplest interface for configuration. 17 + 18 + This doesn't care about how data is loaded, stored, or saved. It simply provides 19 + a bare-bones interface to accessing data, without needing to worry about the nitty-gritty stuff. 20 + ++/ 21 interface IConfig(T) 22 if(is(T == struct) || is(T == class)) 23 { 24 public 25 { 26 /// Loads the configuration. This should overwrite any unsaved changes. 27 void load(); 28 29 /// Saves the configuration. 30 void save(); 31 32 /// Returns: The current value for this configuration. 33 @property 34 T value(); 35 36 /// Sets the configuration's value. 37 @property 38 void value(T value); 39 } 40 41 public final 42 { 43 /++ 44 + Edit the value of this configuration using the provided `editFunc`, optionally 45 + saving if no exceptions are thrown, and optionally rolling back any changes in the case an exception $(B is) thrown. 46 + 47 + Notes: 48 + Exceptions can be caught during either `editFunc`, or a call to `save`. 49 + 50 + Functionally, "rolling back on success" simply means the configuration's `value[set]` property is never used. 51 + 52 + This has a consequence - if your `editFunc` modifies the internal state of the value in a way that takes immediate effect on 53 + the original value (e.g. the value is a class type, so all changes will affect the original value), then "rolling back" won't 54 + be able to prevent any data changes. 55 + 56 + Therefor, it's best to use structs for your configuration types if you're wanting to make use of "rolling back". 57 + 58 + If an error occurs, then `UserIO.verboseException` is used to display the exception. 59 + 60 + $(B Ensure your lambda parameter is marked `scope ref`, otherwise you'll get a compiler error.) 61 + 62 + Params: 63 + editFunc = The function that will edit the configuration's value. 64 + rollback = If `RollbackOnFailure.yes`, then should an error occur, the configuration's value will be left unchanged. 65 + save = If `SaveOnSuccess.yes`, then if no errors occur, a call to `save` will be made. 66 + 67 + Returns: 68 + `WasExceptionThrown` to denote whether an error occured or not. 69 + ++/ 70 WasExceptionThrown edit( 71 void delegate(scope ref T value) editFunc, 72 RollbackOnFailure rollback = RollbackOnFailure.yes, 73 SaveOnSuccess save = SaveOnSuccess.no 74 ) 75 { 76 const uneditedValue = this.value; 77 T value = uneditedValue; // So we can update the value in the event of `rollback.no`. 78 try 79 { 80 editFunc(value); // Pass a temporary, so in the event of an error, changes shouldn't be half-committed. 81 82 this.value = value; 83 if(save) 84 this.save(); 85 86 return WasExceptionThrown.no; 87 } 88 catch(Exception ex) 89 { 90 import jaster.cli.userio : UserIO; 91 UserIO.verboseException(ex); 92 93 this.value = (rollback) ? uneditedValue : value; 94 return WasExceptionThrown.yes; 95 } 96 } 97 98 /// Exactly the same as `edit`, except with the `save` parameter set to `yes`. 99 void editAndSave(void delegate(scope ref T value) editFunc) 100 { 101 this.edit(editFunc, RollbackOnFailure.yes, SaveOnSuccess.yes); 102 } 103 104 /// Exactly the same as `edit`, except with the `save` parameter set to `yes`, and `rollback` set to `no`. 105 void editAndSaveNoRollback(void delegate(scope ref T value) editFunc) 106 { 107 this.edit(editFunc, RollbackOnFailure.no, SaveOnSuccess.yes); 108 } 109 110 /// Exactly the same as `edit`, except with the `rollback` paramter set to `no`. 111 void editNoRollback(void delegate(scope ref T value) editFunc) 112 { 113 this.edit(editFunc, RollbackOnFailure.no, SaveOnSuccess.no); 114 } 115 } 116 } 117 /// 118 unittest 119 { 120 // This is mostly a unittest for testing, not as an example, but may as well show it as an example anyway. 121 static struct Conf 122 { 123 string str; 124 int num; 125 } 126 127 auto config = new InMemoryConfig!Conf(); 128 129 // Default: Rollback on failure, don't save on success. 130 // First `edit` fails, so no data should be commited. 131 // Second `edit` passes, so data is edited. 132 // Test to ensure only the second `edit` committed changes. 133 assert(config.edit((scope ref v) { v.str = "Hello"; v.num = 420; throw new Exception(""); }) == WasExceptionThrown.yes); 134 assert(config.edit((scope ref v) { v.num = 21; }) == WasExceptionThrown.no); 135 assert(config.value == Conf(null, 21)); 136 137 // Reset value, check that we didn't actually call `save` yet. 138 config.load(); 139 assert(config.value == Conf.init); 140 141 // Test editAndSave. Save on success, rollback on failure. 142 // No longer need to test rollback's pass case, as that's now proven to work. 143 config.editAndSave((scope ref v) { v.str = "Lalafell"; }); 144 config.value = Conf.init; 145 config.load(); 146 assert(config.value.str == "Lalafell"); 147 148 // Reset value 149 config.value = Conf.init; 150 config.save(); 151 152 // Test editNoRollback, and then we'll have tested the pass & fail cases for saving and rollbacks. 153 config.editNoRollback((scope ref v) { v.str = "Grubby"; throw new Exception(""); }); 154 assert(config.value.str == "Grubby", config.value.str); 155 } 156 157 /++ 158 + A template that evaluates to a bool which determines whether the given `Adapter` can successfully 159 + compile all the code needed to serialise and deserialise the `For` type. 160 + 161 + Adapters: 162 + Certain `IConfig` implementations may provide a level of flexibliity in the sense that they will offload the responsiblity 163 + of serialising/deserialising the configuration onto something called an `Adapter`. 164 + 165 + For the most part, these `Adapters` are likely to simply be that: an adapter for an already existing serialisation library. 166 + 167 + Adapters require two static functions, with the following or compatible signatures: 168 + 169 + ``` 170 + const(ubyte[]) serialise(For)(For value); 171 + 172 + For deserialise(For)(const(ubyte[]) value); 173 + ``` 174 + 175 + Builtin Adapters: 176 + Please note that any adapter that uses a third party library will only be compiled if your own project includes aforementioned library. 177 + 178 + For example, `AsdfConfigAdapter` requires the asdf library, so will only be available if your dub project includes asdf (or specify the `Have_asdf` version). 179 + 180 + e.g. if you want to use `AsdfConfigAdapter`, use a simple `dub add asdf` in your own project and then you're good to go. 181 + 182 + JCLI provides the following adapters by default: 183 + 184 + * `AsdfConfigAdapter` - An adapter for the asdf serialisation library. asdf is marked as an optional package. 185 + 186 + Notes: 187 + If for whatever reason the given `Adapter` cannot compile when being used with the `For` type, this template 188 + will attempt to instigate an error message from the compiler as to why. 189 + 190 + If this template is being used inside a `static assert`, and fails, then the above attempt to provide an error message as to 191 + why the compliation failed will not be shown, as the `static assert is false` error is thrown before the compile has a chance to collect any other error message. 192 + 193 + In such a case, please temporarily rewrite the `static assert` into storing the result of this template into an `enum`, as that should then allow 194 + the compiler to generate the error message. 195 + ++/ 196 template isConfigAdapterFor(Adapter, For) 197 { 198 static if(isConfigAdapterForImpl!(Adapter, For)) 199 enum isConfigAdapterFor = true; 200 else 201 { 202 alias _ErrorfulInstansiation = showAdapterCompilerErrors!(Adapter, For); 203 enum isConfigAdapterFor = false; 204 } 205 } 206 207 private enum isConfigAdapterForImpl(Adapter, For) = 208 __traits(compiles, { const ubyte[] data = Adapter.serialise!For(For.init); }) 209 && __traits(compiles, { const ubyte[] data; For value = Adapter.deserialise!For(data); }); 210 211 private void showAdapterCompilerErrors(Adapter, For)() 212 { 213 const ubyte[] data = Adapter.serialise!For(For.init); 214 For value = Adapter.deserialise!For(data); 215 } 216 217 /// A very simple `IConfig` that simply stores the value in memory. This is mostly only useful for testing. 218 final class InMemoryConfig(For) : IConfig!For 219 if(isCopyable!For) 220 { 221 private For _savedValue; 222 private For _value; 223 224 public override 225 { 226 void save() 227 { 228 this._savedValue = this._value; 229 } 230 231 void load() 232 { 233 this._value = this._savedValue; 234 } 235 236 @property 237 For value() 238 { 239 return this._value; 240 } 241 242 @property 243 void value(For newValue) 244 { 245 this._value = newValue; 246 } 247 } 248 } 249 250 /++ 251 + Returns: 252 + A Singleton `ServiceInfo` describing an `InMemoryConfig` that stores the `For` type. 253 + ++/ 254 ServiceInfo addInMemoryConfig(For)() 255 { 256 return ServiceInfo.asSingleton!(IConfig!For, InMemoryConfig!For); 257 } 258 259 /// ditto. 260 ServiceInfo[] addInMemoryConfig(For)() 261 { 262 services ~= addInMemoryConfig!For(); 263 return services; 264 } 265 266 /++ 267 + An `IConfig` with adapter support that uses the filesystem to store/retrieve its configuration value. 268 + 269 + Notes: 270 + This class will ensure the directory for the file exists. 271 + 272 + This class will always create a backup ".bak" before every write attempt. It however does not 273 + attempt to restore this file in the event of an error. 274 + 275 + If this class' config file doesn't exist, then `load` is no-op, leaving the `value` as `For.init` 276 + 277 + See_Also: 278 + The docs for `isConfigAdapterFor` to learn more about configs with adapter support. 279 + 280 + `addFileConfig` 281 + ++/ 282 final class AdaptableFileConfig(For, Adapter) : IConfig!For 283 if(isConfigAdapterFor!(Adapter, For) && isCopyable!For) 284 { 285 private For _value; 286 private string _path; 287 288 /++ 289 + Throws: 290 + `Exception` if the given `path` is invalid, after being converted into an absolute path. 291 + 292 + Params: 293 + path = The file path to store the configuration file at. This can be relative or absolute. 294 + ++/ 295 this(string path) 296 { 297 import std.exception : enforce; 298 import std.path : absolutePath, isValidPath; 299 300 this._path = path.absolutePath(); 301 enforce(isValidPath(this._path), "The path '"~this._path~"' is invalid"); 302 } 303 304 public override 305 { 306 void save() 307 { 308 import std.file : write, exists, mkdirRecurse, copy; 309 import std.path : dirName, extension, setExtension; 310 311 const pathDir = this._path.dirName; 312 if(!exists(pathDir)) 313 mkdirRecurse(pathDir); 314 315 const backupExt = this._path.extension ~ ".bak"; 316 const backupPath = this._path.setExtension(backupExt); 317 if(exists(this._path)) 318 copy(this._path, backupPath); 319 320 const ubyte[] data = Adapter.serialise!For(this._value); 321 write(this._path, data); 322 } 323 324 void load() 325 { 326 import std.file : exists, read; 327 328 if(!this._path.exists) 329 return; 330 331 this._value = Adapter.deserialise!For(cast(const ubyte[])read(this._path)); 332 } 333 334 @property 335 For value() 336 { 337 return this._value; 338 } 339 340 @property 341 void value(For newValue) 342 { 343 this._value = newValue; 344 } 345 } 346 } 347 348 /++ 349 + Note: 350 + The base type of the resulting service is `IConfig!For`, so ensure that your dependency injected code asks for 351 + `IConfig!For` instead of `AdapatableFileConfig!(For, Adapter)`. 352 + 353 + Returns: 354 + A Singleton `ServiceInfo` describing an `AdapatableFileConfig` that serialises the given `For` type, into a file 355 + using the provided `Adapter` type. 356 + ++/ 357 ServiceInfo addFileConfig(For, Adapter)(string fileName) 358 { 359 return ServiceInfo.asSingleton!( 360 IConfig!For, 361 AdaptableFileConfig!(For, Adapter) 362 )( 363 (ref _) 364 { 365 auto config = new AdaptableFileConfig!(For, Adapter)(fileName); 366 config.load(); 367 368 return config; 369 } 370 ); 371 } 372 373 /// ditto. 374 ServiceInfo[] addFileConfig(For, Adapter)(ref ServiceInfo[] services, string fileName) 375 { 376 services ~= addFileConfig!(For, Adapter)(fileName); 377 return services; 378 }