Quick.Options.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. { ***************************************************************************
  2. Copyright (c) 2015-2019 Kike Pérez
  3. Unit : Quick.Options
  4. Description : Configuration group settings
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 18/10/2019
  8. Modified : 29/10/2019
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.Options;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. Classes,
  26. RTTI,
  27. Quick.RTTI.Utils,
  28. System.TypInfo,
  29. System.SysUtils,
  30. System.Generics.Collections,
  31. System.Json,
  32. Quick.Commons,
  33. Quick.FileMonitor;
  34. type
  35. Required = class(TCustomAttribute);
  36. TValidationCustomAttribute = class(TCustomAttribute)
  37. protected
  38. fErrorMsg : string;
  39. public
  40. property ErrorMsg : string read fErrorMsg write fErrorMsg;
  41. end;
  42. Range = class(TValidationCustomAttribute)
  43. private
  44. fRangeMin : Double;
  45. fRangeMax : Double;
  46. public
  47. constructor Create(aMin, aMax : Integer; const aErrorMsg : string = ''); overload;
  48. constructor Create(aMin, aMax : Double; const aErrorMsg : string = ''); overload;
  49. property Min : Double read fRangeMin write fRangeMax;
  50. property Max : Double read fRangeMax write fRangeMax;
  51. end;
  52. StringLength = class(TValidationCustomAttribute)
  53. private
  54. fMaxLength : Integer;
  55. public
  56. constructor Create(aMaxLength : Integer; const aErrorMsg : string = '');
  57. property MaxLength : Integer read fMaxLength write fMaxLength;
  58. end;
  59. TOptions = class;
  60. TConfigureOptionsProc<T : TOptions> = reference to procedure(aOptions : T);
  61. IOptionsValidator = interface
  62. ['{C6A09F78-8E34-4689-B943-83620437B9EF}']
  63. procedure ValidateOptions;
  64. end;
  65. IOptionsConfigure<T> = interface
  66. ['{49258BEB-A21D-4C64-BA71-767B8DBD4D92}']
  67. //function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc<T>) : IOptionsValidator;
  68. end;
  69. TOptions = class(TInterfacedObject,IOptionsValidator)
  70. private
  71. fName : string;
  72. procedure ValidateRequired(aProperty : TRttiProperty);
  73. procedure ValidateStringLength(aProperty: TRttiProperty; aValidation : StringLength);
  74. procedure ValidateRange(aProperty : TRttiProperty; aValidation : Range);
  75. procedure DoValidateOptions; virtual;
  76. public
  77. constructor Create;
  78. property Name : string read fName write fName;
  79. procedure DefaultValues; virtual; abstract;
  80. function ConfigureOptions<T : TOptions>(aOptionsFunc : TConfigureOptionsProc<T>) : IOptionsValidator;
  81. procedure ValidateOptions;
  82. end;
  83. TOptions<T : TOptions> = record
  84. private
  85. fOptions : T;
  86. public
  87. constructor Create(aOptions : T);
  88. function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc<T>) : IOptionsValidator;
  89. end;
  90. IOptions<T : TOptions> = interface
  91. ['{2779F946-2692-4F74-88AD-F35F5137057A}']
  92. function GetSectionValue : T;
  93. property Value : T read GetSectionValue;
  94. end;
  95. TOptionsClass = class of TOptions;
  96. IOptionsContainer = interface
  97. ['{A593C8BB-53CF-4AA4-9641-BF974E45CBD1}']
  98. function AddSection(aOption : TOptionsClass; const aOptionsName : string = '') : TOptions;
  99. function GetOptions(aOptionClass : TOptionsClass): TOptions;
  100. function GetSection(aOptionsSection : TOptionsClass; aOptions : TOptions) : Boolean; overload;
  101. end;
  102. TSectionList = TObjectList<TOptions>;
  103. IOptionsSerializer = interface
  104. ['{7DECE203-4AAE-4C9D-86C8-B3D583DF7C8B}']
  105. procedure Load(const aFilename : string; aSections : TSectionList);
  106. procedure Save(const aFilename : string; aSections : TSectionList);
  107. end;
  108. TOptionsSerializer = class(TInterfacedObject,IOptionsSerializer)
  109. public
  110. procedure Load(const aFilename : string; aSections : TSectionList); virtual; abstract;
  111. procedure Save(const aFilename : string; aSections : TSectionList); virtual; abstract;
  112. end;
  113. TFileModifiedEvent = reference to procedure;
  114. TLoadConfigEvent = reference to procedure;
  115. TOptionValue<T : TOptions> = class(TInterfacedObject,IOptions<T>)
  116. private
  117. fValue : T;
  118. function GetSectionValue : T;
  119. public
  120. constructor Create(aValue : T);
  121. property Value : T read GetSectionValue;
  122. end;
  123. TOptionsContainer = class(TInterfacedObject,IOptionsContainer)
  124. private
  125. fFilename : string;
  126. fSerializer : IOptionsSerializer;
  127. fSections : TSectionList;
  128. fFileMonitor : TFileMonitor;
  129. fOnFileModified : TFileModifiedEvent;
  130. fLoaded : Boolean;
  131. fReloadIfFileChanged : Boolean;
  132. fOnConfigLoaded : TLoadConfigEvent;
  133. fOnConfigReloaded : TLoadConfigEvent;
  134. procedure CreateFileMonitor;
  135. procedure FileModifiedNotify(MonitorNotify : TMonitorNotify);
  136. procedure SetReloadIfFileChanged(const Value: Boolean);
  137. function GetOptions(aOptionClass : TOptionsClass): TOptions; overload;
  138. function GetOptions(aIndex : Integer) : TOptions; overload;
  139. function GetSection(aOptionsSection : TOptionsClass; aOptions : TOptions) : Boolean; overload;
  140. public
  141. constructor Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False);
  142. destructor Destroy; override;
  143. property FileName : string read fFilename write fFilename;
  144. property ReloadIfFileChanged : Boolean read fReloadIfFileChanged write SetReloadIfFileChanged;
  145. property IsLoaded : Boolean read fLoaded;
  146. property OnFileModified : TFileModifiedEvent read fOnFileModified write fOnFileModified;
  147. property OnConfigLoaded : TLoadConfigEvent read fOnConfigLoaded write fOnConfigLoaded;
  148. property OnConfigReloaded : TLoadConfigEvent read fOnConfigReloaded write fOnConfigReloaded;
  149. property Items[aOptionClass : TOptionsClass] : TOptions read GetOptions; default;
  150. property Items[aIndex : Integer] : TOptions read GetOptions; default;
  151. function AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions; overload;
  152. function AddSection<T : TOptions>(const aSectionName : string = '') : TOptions<T>; overload;
  153. function GetSectionInterface<T : TOptions> : IOptions<T>;
  154. function GetSection<T : TOptions>(const aSectionName : string = '') : T; overload;
  155. function Count : Integer;
  156. procedure Load;
  157. procedure Save;
  158. end;
  159. IOptionsBuilder<T : TOptions> = interface
  160. ['{1A1DC9A9-7F2D-4CC4-A772-6C7DBAB34424}']
  161. function Options : T;
  162. end;
  163. TOptionsBuilder<T : TOptions> = class(TInterfacedObject,IOptionsBuilder<T>)
  164. protected
  165. fOptions : T;
  166. public
  167. constructor Create;
  168. function Options : T;
  169. end;
  170. EOptionConfigureError = class(Exception);
  171. EOptionLoadError = class(Exception);
  172. EOptionSaveError = class(Exception);
  173. EOptionValidationError = class(Exception);
  174. implementation
  175. { TOptionsContainer }
  176. constructor TOptionsContainer.Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False);
  177. begin
  178. fSerializer := aOptionsSerializer;
  179. fSections := TSectionList.Create(True);
  180. fFilename := aFilename;
  181. fLoaded := False;
  182. fReloadIfFileChanged := aReloadIfFileChanged;
  183. if aReloadIfFileChanged then CreateFileMonitor;
  184. end;
  185. procedure TOptionsContainer.CreateFileMonitor;
  186. begin
  187. fFileMonitor := TQuickFileMonitor.Create;
  188. fFileMonitor.FileName := fFilename;
  189. fFileMonitor.Interval := 2000;
  190. fFileMonitor.Notifies := [TMonitorNotify.mnFileModified];
  191. fFileMonitor.OnFileChange := FileModifiedNotify;
  192. fFileMonitor.Enabled := True;
  193. end;
  194. destructor TOptionsContainer.Destroy;
  195. begin
  196. if Assigned(fFileMonitor) then fFileMonitor.Free;
  197. fSerializer := nil;
  198. fSections.Free;
  199. inherited;
  200. end;
  201. procedure TOptionsContainer.FileModifiedNotify(MonitorNotify: TMonitorNotify);
  202. begin
  203. if MonitorNotify = TMonitorNotify.mnFileModified then
  204. begin
  205. if Assigned(fOnFileModified) then fOnFileModified;
  206. if fReloadIfFileChanged then
  207. begin
  208. Load;
  209. end;
  210. end;
  211. end;
  212. function TOptionsContainer.AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions;
  213. var
  214. option : TOptions;
  215. begin
  216. option := aOption.Create;
  217. if aSectionName.IsEmpty then option.Name := Copy(aOption.ClassName,2,aOption.ClassName.Length)
  218. else option.Name := aSectionName;
  219. fSections.Add(option);
  220. Result := option;
  221. end;
  222. function TOptionsContainer.AddSection<T>(const aSectionName: string): TOptions<T>;
  223. var
  224. option : TOptions;
  225. begin
  226. option := TRTTI.CreateInstance<T>;
  227. if aSectionName.IsEmpty then option.Name := Copy(T.ClassName,2,T.ClassName.Length)
  228. else option.Name := aSectionName;
  229. fSections.Add(option);
  230. Result.Create(option);
  231. end;
  232. function TOptionsContainer.Count: Integer;
  233. begin
  234. Result := fSections.Count;
  235. end;
  236. function TOptionsContainer.GetOptions(aIndex: Integer): TOptions;
  237. begin
  238. Result := fSections[aIndex];
  239. end;
  240. function TOptionsContainer.GetSection(aOptionsSection: TOptionsClass; aOptions: TOptions): Boolean;
  241. var
  242. option : TOptions;
  243. begin
  244. Result := False;
  245. for option in fSections do
  246. begin
  247. if option is TOptionsClass then
  248. begin
  249. aOptions := option as TOptionsClass;
  250. Exit;
  251. end;
  252. end;
  253. end;
  254. function TOptionsContainer.GetOptions(aOptionClass : TOptionsClass) : TOptions;
  255. var
  256. option : TOptions;
  257. begin
  258. Result := nil;
  259. for option in fSections do
  260. begin
  261. if option is TOptionsClass then Result := option as TOptionsClass;
  262. end;
  263. end;
  264. function TOptionsContainer.GetSection<T>(const aSectionName : string = '') : T;
  265. var
  266. option : TOptions;
  267. begin
  268. for option in fSections do
  269. begin
  270. if option is T then
  271. begin
  272. if (aSectionName.IsEmpty) or (CompareText(option.Name,aSectionName) = 0) then
  273. begin
  274. Result := option as T;
  275. Exit;
  276. end;
  277. end;
  278. end;
  279. end;
  280. function TOptionsContainer.GetSectionInterface<T>: IOptions<T>;
  281. begin
  282. Result := TOptionValue<T>.Create(Self.GetSection<T>);
  283. end;
  284. procedure TOptionsContainer.Load;
  285. var
  286. option : TOptions;
  287. begin
  288. if FileExists(fFilename) then
  289. begin
  290. fSerializer.Load(fFilename,fSections);
  291. if not fLoaded then
  292. begin
  293. fLoaded := True;
  294. if Assigned(fOnConfigLoaded) then fOnConfigLoaded;
  295. end
  296. else if Assigned(fOnConfigReloaded) then fOnConfigReloaded;
  297. end
  298. else
  299. begin
  300. //if not exists file get default values
  301. for option in fSections do option.DefaultValues;
  302. //creates default file
  303. Save;
  304. end;
  305. end;
  306. procedure TOptionsContainer.Save;
  307. var
  308. laststate : Boolean;
  309. begin
  310. //disable filemonitor to avoid detect manual save as a external file change
  311. laststate := fFileMonitor.Enabled;
  312. fFileMonitor.Enabled := False;
  313. try
  314. //save config file
  315. fSerializer.Save(fFilename,fSections);
  316. finally
  317. //set last state
  318. fFileMonitor.Enabled := laststate;
  319. end;
  320. end;
  321. procedure TOptionsContainer.SetReloadIfFileChanged(const Value: Boolean);
  322. begin
  323. if Value = fReloadIfFileChanged then Exit;
  324. fReloadIfFileChanged := Value;
  325. if Assigned(fFileMonitor) then fFileMonitor.Free;
  326. if fReloadIfFileChanged then CreateFileMonitor;
  327. end;
  328. { TOptions }
  329. function TOptions.ConfigureOptions<T>(aOptionsFunc: TConfigureOptionsProc<T>): IOptionsValidator;
  330. var
  331. value : TValue;
  332. begin
  333. Result := Self;
  334. if Assigned(aOptionsFunc) then
  335. begin
  336. value := Self;
  337. aOptionsFunc(value.AsType<T>);
  338. end;
  339. end;
  340. constructor TOptions.Create;
  341. begin
  342. fName := '';
  343. end;
  344. procedure TOptions.DoValidateOptions;
  345. var
  346. ctx : TRttiContext;
  347. rtype : TRttiType;
  348. rprop : TRttiProperty;
  349. attrib : TCustomAttribute;
  350. begin
  351. ctx := TRttiContext.Create;
  352. try
  353. rtype := ctx.GetType(Self.ClassInfo);
  354. for rprop in rtype.GetProperties do
  355. begin
  356. //check only published properties
  357. if rprop.Visibility = TMemberVisibility.mvPublished then
  358. begin
  359. //check validation option attributes
  360. for attrib in rprop.GetAttributes do
  361. begin
  362. if attrib is Required then ValidateRequired(rprop)
  363. else if attrib is StringLength then ValidateStringLength(rprop,StringLength(attrib))
  364. else if attrib is Range then ValidateRange(rprop,Range(attrib));
  365. end;
  366. end;
  367. end;
  368. finally
  369. ctx.Free;
  370. end;
  371. end;
  372. procedure TOptions.ValidateOptions;
  373. begin
  374. try
  375. DoValidateOptions;
  376. except
  377. on E : Exception do
  378. begin
  379. raise EOptionConfigureError.CreateFmt('Validation Options Error : %s',[e.Message]);
  380. end;
  381. end;
  382. end;
  383. procedure TOptions.ValidateRange(aProperty: TRttiProperty; aValidation : Range);
  384. var
  385. value : TValue;
  386. msg : string;
  387. begin
  388. value := aProperty.GetValue(Self);
  389. if not value.IsEmpty then
  390. begin
  391. if value.Kind = tkFloat then
  392. begin
  393. if (value.AsExtended < aValidation.Min) or (value.AsExtended > aValidation.Max) then
  394. begin
  395. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option "%s.%s" exceeds predefined range (%2f - %2f)',[Self.Name,aProperty.Name,aValidation.Min,aValidation.Max])
  396. else msg := aValidation.ErrorMsg;
  397. raise EOptionValidationError.Create(msg);
  398. end;
  399. end
  400. else if value.Kind in [tkInteger,tkInt64] then
  401. begin
  402. if (value.AsInt64 < aValidation.Min) or (value.AsInt64 > aValidation.Max) then
  403. begin
  404. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option "%s.%s" exceeds predefined range (%d - %d)',[Self.Name,aProperty.Name,Round(aValidation.Min),Round(aValidation.Max)])
  405. else msg := aValidation.ErrorMsg;
  406. raise EOptionValidationError.Create(msg);
  407. end;
  408. end;
  409. end;
  410. end;
  411. procedure TOptions.ValidateRequired(aProperty: TRttiProperty);
  412. begin
  413. if aProperty.GetValue(Self).IsEmpty then raise EOptionValidationError.CreateFmt('Option "%s.%s" is required',[Self.Name,aProperty.Name]);
  414. end;
  415. procedure TOptions.ValidateStringLength(aProperty: TRttiProperty; aValidation : StringLength);
  416. var
  417. value : TValue;
  418. msg : string;
  419. begin
  420. value := aProperty.GetValue(Self);
  421. if (not value.IsEmpty) and (value.AsString.Length > aValidation.MaxLength) then
  422. begin
  423. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option "%s.%s" exceeds max length (%d)',[Self.Name,aProperty.Name,aValidation.MaxLength])
  424. else msg := aValidation.ErrorMsg;
  425. raise EOptionValidationError.Create(msg);
  426. end;
  427. end;
  428. { Range }
  429. constructor Range.Create(aMin, aMax: Integer; const aErrorMsg : string = '');
  430. begin
  431. fRangeMin := aMin;
  432. fRangeMax := aMax;
  433. fErrorMsg := aErrorMsg;
  434. end;
  435. constructor Range.Create(aMin, aMax: Double; const aErrorMsg: string);
  436. begin
  437. fRangeMin := aMin;
  438. fRangeMax := aMax;
  439. fErrorMsg := aErrorMsg;
  440. end;
  441. { StringLength }
  442. constructor StringLength.Create(aMaxLength: Integer; const aErrorMsg : string = '');
  443. begin
  444. fMaxLength := aMaxLength;
  445. fErrorMsg := aErrorMsg;
  446. end;
  447. { TOptionValue<T> }
  448. constructor TOptionValue<T>.Create(aValue: T);
  449. begin
  450. fValue := aValue;
  451. end;
  452. function TOptionValue<T>.GetSectionValue: T;
  453. begin
  454. Result := fValue;
  455. end;
  456. { TOptions<T> }
  457. function TOptions<T>.ConfigureOptions(aOptionsFunc: TConfigureOptionsProc<T>): IOptionsValidator;
  458. begin
  459. if Assigned(aOptionsFunc) then Result := fOptions.ConfigureOptions<T>(aOptionsFunc)
  460. else Result := fOptions;
  461. fOptions._AddRef;
  462. end;
  463. constructor TOptions<T>.Create(aOptions: T);
  464. begin
  465. fOptions := aOptions;
  466. end;
  467. { TOptionsBuilder<T> }
  468. constructor TOptionsBuilder<T>.Create;
  469. begin
  470. fOptions := (PTypeInfo(TypeInfo(T)).TypeData.ClassType.Create) as T;
  471. end;
  472. function TOptionsBuilder<T>.Options: T;
  473. begin
  474. Result := fOptions;
  475. end;
  476. end.