Quick.Options.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  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 : 16/12/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(const aInstance : TObject; aProperty: TRttiProperty);
  73. procedure ValidateStringLength(const aInstance : TObject; aProperty: TRttiProperty; aValidation : StringLength);
  74. procedure ValidateRange(const aInstance : TObject; aProperty: TRttiProperty; aValidation : Range);
  75. procedure DoValidateOptions; virtual;
  76. procedure ValidateObject(aObj : TObject);
  77. procedure ValidateArray(aValue : TValue);
  78. public
  79. constructor Create;
  80. property Name : string read fName write fName;
  81. procedure DefaultValues; virtual;
  82. function ConfigureOptions<T : TOptions>(aOptionsFunc : TConfigureOptionsProc<T>) : IOptionsValidator;
  83. procedure ValidateOptions;
  84. end;
  85. TOptions<T : TOptions> = record
  86. private
  87. fOptions : T;
  88. public
  89. constructor Create(aOptions : T);
  90. function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc<T>) : IOptionsValidator;
  91. end;
  92. IOptions<T : TOptions> = interface
  93. ['{2779F946-2692-4F74-88AD-F35F5137057A}']
  94. function GetSectionValue : T;
  95. property Value : T read GetSectionValue;
  96. end;
  97. TOptionsClass = class of TOptions;
  98. IOptionsContainer = interface
  99. ['{A593C8BB-53CF-4AA4-9641-BF974E45CBD1}']
  100. function AddSection(aOption : TOptionsClass; const aOptionsName : string = '') : TOptions;
  101. function GetOptions(aOptionClass : TOptionsClass): TOptions;
  102. function GetSection(aOptionsSection : TOptionsClass; var vOptions : TOptions) : Boolean; overload;
  103. end;
  104. TSectionList = TObjectList<TOptions>;
  105. IOptionsSerializer = interface
  106. ['{7DECE203-4AAE-4C9D-86C8-B3D583DF7C8B}']
  107. function Load(const aFilename : string; aSections : TSectionList; aFailOnSectionNotExists : Boolean) : Boolean;
  108. procedure Save(const aFilename : string; aSections : TSectionList);
  109. end;
  110. TOptionsSerializer = class(TInterfacedObject,IOptionsSerializer)
  111. public
  112. function Load(const aFilename : string; aSections : TSectionList; aFailOnSectionNotExists : Boolean) : Boolean; virtual; abstract;
  113. procedure Save(const aFilename : string; aSections : TSectionList); virtual; abstract;
  114. end;
  115. TFileModifiedEvent = reference to procedure;
  116. TLoadConfigEvent = reference to procedure;
  117. TOptionValue<T : TOptions> = class(TInterfacedObject,IOptions<T>)
  118. private
  119. fValue : T;
  120. function GetSectionValue : T;
  121. public
  122. constructor Create(aValue : T);
  123. property Value : T read GetSectionValue;
  124. end;
  125. TOptionsContainer = class(TInterfacedObject,IOptionsContainer)
  126. private
  127. fFilename : string;
  128. fSerializer : IOptionsSerializer;
  129. fSections : TSectionList;
  130. fFileMonitor : TFileMonitor;
  131. fOnFileModified : TFileModifiedEvent;
  132. fLoaded : Boolean;
  133. fReloadIfFileChanged : Boolean;
  134. fOnConfigLoaded : TLoadConfigEvent;
  135. fOnConfigReloaded : TLoadConfigEvent;
  136. procedure CreateFileMonitor;
  137. procedure FileModifiedNotify(MonitorNotify : TMonitorNotify);
  138. procedure SetReloadIfFileChanged(const Value: Boolean);
  139. function GetOptions(aOptionClass : TOptionsClass): TOptions; overload;
  140. function GetOptions(aIndex : Integer) : TOptions; overload;
  141. function GetSection(aOptionsSection : TOptionsClass; var vOptions : TOptions) : Boolean; overload;
  142. public
  143. constructor Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False);
  144. destructor Destroy; override;
  145. property FileName : string read fFilename write fFilename;
  146. property ReloadIfFileChanged : Boolean read fReloadIfFileChanged write SetReloadIfFileChanged;
  147. property IsLoaded : Boolean read fLoaded;
  148. property OnFileModified : TFileModifiedEvent read fOnFileModified write fOnFileModified;
  149. property OnConfigLoaded : TLoadConfigEvent read fOnConfigLoaded write fOnConfigLoaded;
  150. property OnConfigReloaded : TLoadConfigEvent read fOnConfigReloaded write fOnConfigReloaded;
  151. property Items[aOptionClass : TOptionsClass] : TOptions read GetOptions; default;
  152. property Items[aIndex : Integer] : TOptions read GetOptions; default;
  153. function AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions; overload;
  154. function AddSection<T : TOptions>(const aSectionName : string = '') : TOptions<T>; overload;
  155. function GetSectionInterface<T : TOptions> : IOptions<T>;
  156. function GetSection<T : TOptions>(const aSectionName : string = '') : T; overload;
  157. function Count : Integer;
  158. procedure Load(aFailOnSectionNotExists : Boolean = False);
  159. procedure Save;
  160. end;
  161. IOptionsBuilder<T : TOptions> = interface
  162. ['{1A1DC9A9-7F2D-4CC4-A772-6C7DBAB34424}']
  163. function Options : T;
  164. end;
  165. TOptionsBuilder<T : TOptions> = class(TInterfacedObject,IOptionsBuilder<T>)
  166. protected
  167. fOptions : T;
  168. public
  169. constructor Create;
  170. function Options : T;
  171. end;
  172. EOptionConfigureError = class(Exception);
  173. EOptionLoadError = class(Exception);
  174. EOptionSaveError = class(Exception);
  175. EOptionValidationError = class(Exception);
  176. implementation
  177. { TOptionsContainer }
  178. constructor TOptionsContainer.Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False);
  179. begin
  180. fSerializer := aOptionsSerializer;
  181. fSections := TSectionList.Create(True);
  182. fFilename := aFilename;
  183. fLoaded := False;
  184. fReloadIfFileChanged := aReloadIfFileChanged;
  185. if aReloadIfFileChanged then CreateFileMonitor;
  186. end;
  187. procedure TOptionsContainer.CreateFileMonitor;
  188. begin
  189. fFileMonitor := TQuickFileMonitor.Create;
  190. fFileMonitor.FileName := fFilename;
  191. fFileMonitor.Interval := 2000;
  192. fFileMonitor.Notifies := [TMonitorNotify.mnFileModified];
  193. fFileMonitor.OnFileChange := FileModifiedNotify;
  194. fFileMonitor.Enabled := True;
  195. end;
  196. destructor TOptionsContainer.Destroy;
  197. begin
  198. if Assigned(fFileMonitor) then fFileMonitor.Free;
  199. fSerializer := nil;
  200. fSections.Free;
  201. inherited;
  202. end;
  203. procedure TOptionsContainer.FileModifiedNotify(MonitorNotify: TMonitorNotify);
  204. begin
  205. if MonitorNotify = TMonitorNotify.mnFileModified then
  206. begin
  207. if Assigned(fOnFileModified) then fOnFileModified;
  208. if fReloadIfFileChanged then
  209. begin
  210. Load(False);
  211. end;
  212. end;
  213. end;
  214. function TOptionsContainer.AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions;
  215. var
  216. option : TOptions;
  217. begin
  218. option := aOption.Create;
  219. if aSectionName.IsEmpty then option.Name := Copy(aOption.ClassName,2,aOption.ClassName.Length)
  220. else option.Name := aSectionName;
  221. fSections.Add(option);
  222. Result := option;
  223. end;
  224. function TOptionsContainer.AddSection<T>(const aSectionName: string): TOptions<T>;
  225. var
  226. option : TOptions;
  227. begin
  228. option := TRTTI.CreateInstance<T>;
  229. if aSectionName.IsEmpty then option.Name := Copy(T.ClassName,2,T.ClassName.Length)
  230. else option.Name := aSectionName;
  231. fSections.Add(option);
  232. Result.Create(option);
  233. end;
  234. function TOptionsContainer.Count: Integer;
  235. begin
  236. Result := fSections.Count;
  237. end;
  238. function TOptionsContainer.GetOptions(aIndex: Integer): TOptions;
  239. begin
  240. Result := fSections[aIndex];
  241. end;
  242. function TOptionsContainer.GetSection(aOptionsSection: TOptionsClass; var vOptions: TOptions): Boolean;
  243. var
  244. option : TOptions;
  245. begin
  246. Result := False;
  247. for option in fSections do
  248. begin
  249. if option is TOptionsClass then
  250. begin
  251. vOptions := option as TOptionsClass;
  252. Exit;
  253. end;
  254. end;
  255. end;
  256. function TOptionsContainer.GetOptions(aOptionClass : TOptionsClass) : TOptions;
  257. var
  258. option : TOptions;
  259. begin
  260. Result := nil;
  261. for option in fSections do
  262. begin
  263. if option is TOptionsClass then Result := option as TOptionsClass;
  264. end;
  265. end;
  266. function TOptionsContainer.GetSection<T>(const aSectionName : string = '') : T;
  267. var
  268. option : TOptions;
  269. begin
  270. for option in fSections do
  271. begin
  272. if option is T then
  273. begin
  274. if (aSectionName.IsEmpty) or (CompareText(option.Name,aSectionName) = 0) then
  275. begin
  276. Result := option as T;
  277. Exit;
  278. end;
  279. end;
  280. end;
  281. end;
  282. function TOptionsContainer.GetSectionInterface<T>: IOptions<T>;
  283. begin
  284. Result := TOptionValue<T>.Create(Self.GetSection<T>);
  285. end;
  286. procedure TOptionsContainer.Load(aFailOnSectionNotExists : Boolean = False);
  287. var
  288. option : TOptions;
  289. begin
  290. if FileExists(fFilename) then
  291. begin
  292. if not fSerializer.Load(fFilename,fSections,aFailOnSectionNotExists) then Save;
  293. if not fLoaded then
  294. begin
  295. fLoaded := True;
  296. if Assigned(fOnConfigLoaded) then fOnConfigLoaded;
  297. end
  298. else if Assigned(fOnConfigReloaded) then fOnConfigReloaded;
  299. end
  300. else
  301. begin
  302. //if not exists file get default values
  303. for option in fSections do option.DefaultValues;
  304. //creates default file
  305. Save;
  306. end;
  307. end;
  308. procedure TOptionsContainer.Save;
  309. var
  310. laststate : Boolean;
  311. begin
  312. //disable filemonitor to avoid detect manual save as a external file change
  313. laststate := fFileMonitor.Enabled;
  314. fFileMonitor.Enabled := False;
  315. try
  316. //save config file
  317. fSerializer.Save(fFilename,fSections);
  318. finally
  319. //set last state
  320. fFileMonitor.Enabled := laststate;
  321. end;
  322. end;
  323. procedure TOptionsContainer.SetReloadIfFileChanged(const Value: Boolean);
  324. begin
  325. if Value = fReloadIfFileChanged then Exit;
  326. fReloadIfFileChanged := Value;
  327. if Assigned(fFileMonitor) then fFileMonitor.Free;
  328. if fReloadIfFileChanged then CreateFileMonitor;
  329. end;
  330. { TOptions }
  331. function TOptions.ConfigureOptions<T>(aOptionsFunc: TConfigureOptionsProc<T>): IOptionsValidator;
  332. var
  333. value : TValue;
  334. begin
  335. Result := Self;
  336. if Assigned(aOptionsFunc) then
  337. begin
  338. value := Self;
  339. aOptionsFunc(value.AsType<T>);
  340. end;
  341. end;
  342. constructor TOptions.Create;
  343. begin
  344. fName := '';
  345. end;
  346. procedure TOptions.DefaultValues;
  347. begin
  348. //nothing
  349. end;
  350. procedure TOptions.DoValidateOptions;
  351. begin
  352. ValidateObject(Self);
  353. end;
  354. procedure TOptions.ValidateObject(aObj : TObject);
  355. var
  356. ctx : TRttiContext;
  357. rtype : TRttiType;
  358. rprop : TRttiProperty;
  359. attrib : TCustomAttribute;
  360. rvalue : TValue;
  361. begin
  362. ctx := TRttiContext.Create;
  363. try
  364. rtype := ctx.GetType(aObj.ClassInfo);
  365. for rprop in rtype.GetProperties do
  366. begin
  367. //check only published properties
  368. if rprop.Visibility = TMemberVisibility.mvPublished then
  369. begin
  370. //check validation option attributes
  371. for attrib in rprop.GetAttributes do
  372. begin
  373. if attrib is Required then ValidateRequired(aObj,rprop)
  374. else if attrib is StringLength then ValidateStringLength(aObj,rprop,StringLength(attrib))
  375. else if attrib is Range then ValidateRange(aObj,rprop,Range(attrib));
  376. end;
  377. rvalue := rprop.GetValue(aObj);
  378. if not rvalue.IsEmpty then
  379. begin
  380. case rvalue.Kind of
  381. tkClass : ValidateObject(rvalue.AsObject);
  382. tkDynArray : ValidateArray(rvalue);
  383. end;
  384. end;
  385. end;
  386. end;
  387. finally
  388. ctx.Free;
  389. end;
  390. end;
  391. procedure TOptions.ValidateArray(aValue : TValue);
  392. type
  393. PPByte = ^PByte;
  394. var
  395. ctx : TRttiContext;
  396. rDynArray : TRttiDynamicArrayType;
  397. itvalue : TValue;
  398. i : Integer;
  399. begin
  400. ctx := TRttiContext.Create;
  401. try
  402. rDynArray := ctx.GetType(aValue.TypeInfo) as TRTTIDynamicArrayType;
  403. for i := 0 to aValue.GetArrayLength - 1 do
  404. begin
  405. TValue.Make(PPByte(aValue.GetReferenceToRawData)^ + rDynArray.ElementType.TypeSize * i, rDynArray.ElementType.Handle,itvalue);
  406. if not itvalue.IsEmpty then
  407. begin
  408. case itvalue.Kind of
  409. tkClass : ValidateObject(itvalue.AsObject);
  410. tkDynArray : ValidateArray(itvalue);
  411. end;
  412. end;
  413. end;
  414. finally
  415. ctx.Free;
  416. end;
  417. end;
  418. procedure TOptions.ValidateOptions;
  419. begin
  420. try
  421. DoValidateOptions;
  422. except
  423. on E : Exception do
  424. begin
  425. raise EOptionConfigureError.CreateFmt('Validation Options Error : %s',[e.Message]);
  426. end;
  427. end;
  428. end;
  429. procedure TOptions.ValidateRange(const aInstance : TObject; aProperty: TRttiProperty; aValidation : Range);
  430. var
  431. value : TValue;
  432. msg : string;
  433. begin
  434. value := aProperty.GetValue(aInstance);
  435. if not value.IsEmpty then
  436. begin
  437. if value.Kind = tkFloat then
  438. begin
  439. if (value.AsExtended < aValidation.Min) or (value.AsExtended > aValidation.Max) then
  440. begin
  441. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option %s "%s.%s" exceeds predefined range (%2f - %2f)',[Self.Name,aInstance.ClassName,aProperty.Name,aValidation.Min,aValidation.Max])
  442. else msg := aValidation.ErrorMsg;
  443. raise EOptionValidationError.Create(msg);
  444. end;
  445. end
  446. else if value.Kind in [tkInteger,tkInt64] then
  447. begin
  448. if (value.AsInt64 < aValidation.Min) or (value.AsInt64 > aValidation.Max) then
  449. begin
  450. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option %s "%s.%s" exceeds predefined range (%d - %d)',[Self.Name,aInstance.ClassName,aProperty.Name,Round(aValidation.Min),Round(aValidation.Max)])
  451. else msg := aValidation.ErrorMsg;
  452. raise EOptionValidationError.Create(msg);
  453. end;
  454. end;
  455. end;
  456. end;
  457. procedure TOptions.ValidateRequired(const aInstance : TObject; aProperty: TRttiProperty);
  458. begin
  459. if aProperty.GetValue(aInstance).IsEmpty then raise EOptionValidationError.CreateFmt('Option %s "%s.%s" is required',[Self.Name,aInstance.ClassName,aProperty.Name]);
  460. end;
  461. procedure TOptions.ValidateStringLength(const aInstance : TObject; aProperty: TRttiProperty; aValidation : StringLength);
  462. var
  463. value : TValue;
  464. msg : string;
  465. begin
  466. value := aProperty.GetValue(aInstance);
  467. if (not value.IsEmpty) and (value.AsString.Length > aValidation.MaxLength) then
  468. begin
  469. if aValidation.ErrorMsg.IsEmpty then msg := Format('Option %s "%s.%s" exceeds max length (%d)',[Self.Name,aInstance.ClassName,aProperty.Name,aValidation.MaxLength])
  470. else msg := aValidation.ErrorMsg;
  471. raise EOptionValidationError.Create(msg);
  472. end;
  473. end;
  474. { Range }
  475. constructor Range.Create(aMin, aMax: Integer; const aErrorMsg : string = '');
  476. begin
  477. fRangeMin := aMin;
  478. fRangeMax := aMax;
  479. fErrorMsg := aErrorMsg;
  480. end;
  481. constructor Range.Create(aMin, aMax: Double; const aErrorMsg: string);
  482. begin
  483. fRangeMin := aMin;
  484. fRangeMax := aMax;
  485. fErrorMsg := aErrorMsg;
  486. end;
  487. { StringLength }
  488. constructor StringLength.Create(aMaxLength: Integer; const aErrorMsg : string = '');
  489. begin
  490. fMaxLength := aMaxLength;
  491. fErrorMsg := aErrorMsg;
  492. end;
  493. { TOptionValue<T> }
  494. constructor TOptionValue<T>.Create(aValue: T);
  495. begin
  496. fValue := aValue;
  497. end;
  498. function TOptionValue<T>.GetSectionValue: T;
  499. begin
  500. Result := fValue;
  501. end;
  502. { TOptions<T> }
  503. function TOptions<T>.ConfigureOptions(aOptionsFunc: TConfigureOptionsProc<T>): IOptionsValidator;
  504. begin
  505. if Assigned(aOptionsFunc) then Result := fOptions.ConfigureOptions<T>(aOptionsFunc)
  506. else Result := fOptions;
  507. fOptions._AddRef;
  508. end;
  509. constructor TOptions<T>.Create(aOptions: T);
  510. begin
  511. fOptions := aOptions;
  512. end;
  513. { TOptionsBuilder<T> }
  514. constructor TOptionsBuilder<T>.Create;
  515. begin
  516. fOptions := (PTypeInfo(TypeInfo(T)).TypeData.ClassType.Create) as T;
  517. end;
  518. function TOptionsBuilder<T>.Options: T;
  519. begin
  520. Result := fOptions;
  521. end;
  522. end.