{ *************************************************************************** Copyright (c) 2015-2019 Kike Pérez Unit : Quick.Options Description : Configuration group settings Author : Kike Pérez Version : 1.0 Created : 18/10/2019 Modified : 29/10/2019 This file is part of QuickLib: https://github.com/exilon/QuickLib *************************************************************************** Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. *************************************************************************** } unit Quick.Options; {$i QuickLib.inc} interface uses Classes, RTTI, Quick.RTTI.Utils, System.TypInfo, System.SysUtils, System.Generics.Collections, System.Json, Quick.Commons, Quick.FileMonitor; type Required = class(TCustomAttribute); TValidationCustomAttribute = class(TCustomAttribute) protected fErrorMsg : string; public property ErrorMsg : string read fErrorMsg write fErrorMsg; end; Range = class(TValidationCustomAttribute) private fRangeMin : Double; fRangeMax : Double; public constructor Create(aMin, aMax : Integer; const aErrorMsg : string = ''); overload; constructor Create(aMin, aMax : Double; const aErrorMsg : string = ''); overload; property Min : Double read fRangeMin write fRangeMax; property Max : Double read fRangeMax write fRangeMax; end; StringLength = class(TValidationCustomAttribute) private fMaxLength : Integer; public constructor Create(aMaxLength : Integer; const aErrorMsg : string = ''); property MaxLength : Integer read fMaxLength write fMaxLength; end; TOptions = class; TConfigureOptionsProc = reference to procedure(aOptions : T); IOptionsValidator = interface ['{C6A09F78-8E34-4689-B943-83620437B9EF}'] procedure ValidateOptions; end; IOptionsConfigure = interface ['{49258BEB-A21D-4C64-BA71-767B8DBD4D92}'] //function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc) : IOptionsValidator; end; TOptions = class(TInterfacedObject,IOptionsValidator) private fName : string; procedure ValidateRequired(aProperty : TRttiProperty); procedure ValidateStringLength(aProperty: TRttiProperty; aValidation : StringLength); procedure ValidateRange(aProperty : TRttiProperty; aValidation : Range); procedure DoValidateOptions; virtual; public constructor Create; property Name : string read fName write fName; procedure DefaultValues; virtual; abstract; function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc) : IOptionsValidator; procedure ValidateOptions; end; TOptions = record private fOptions : T; public constructor Create(aOptions : T); function ConfigureOptions(aOptionsFunc : TConfigureOptionsProc) : IOptionsValidator; end; IOptions = interface ['{2779F946-2692-4F74-88AD-F35F5137057A}'] function GetSectionValue : T; property Value : T read GetSectionValue; end; TOptionsClass = class of TOptions; IOptionsContainer = interface ['{A593C8BB-53CF-4AA4-9641-BF974E45CBD1}'] function AddSection(aOption : TOptionsClass; const aOptionsName : string = '') : TOptions; function GetOptions(aOptionClass : TOptionsClass): TOptions; function GetSection(aOptionsSection : TOptionsClass; aOptions : TOptions) : Boolean; overload; end; TSectionList = TObjectList; IOptionsSerializer = interface ['{7DECE203-4AAE-4C9D-86C8-B3D583DF7C8B}'] procedure Load(const aFilename : string; aSections : TSectionList); procedure Save(const aFilename : string; aSections : TSectionList); end; TOptionsSerializer = class(TInterfacedObject,IOptionsSerializer) public procedure Load(const aFilename : string; aSections : TSectionList); virtual; abstract; procedure Save(const aFilename : string; aSections : TSectionList); virtual; abstract; end; TFileModifiedEvent = reference to procedure; TLoadConfigEvent = reference to procedure; TOptionValue = class(TInterfacedObject,IOptions) private fValue : T; function GetSectionValue : T; public constructor Create(aValue : T); property Value : T read GetSectionValue; end; TOptionsContainer = class(TInterfacedObject,IOptionsContainer) private fFilename : string; fSerializer : IOptionsSerializer; fSections : TSectionList; fFileMonitor : TFileMonitor; fOnFileModified : TFileModifiedEvent; fLoaded : Boolean; fReloadIfFileChanged : Boolean; fOnConfigLoaded : TLoadConfigEvent; fOnConfigReloaded : TLoadConfigEvent; procedure CreateFileMonitor; procedure FileModifiedNotify(MonitorNotify : TMonitorNotify); procedure SetReloadIfFileChanged(const Value: Boolean); function GetOptions(aOptionClass : TOptionsClass): TOptions; overload; function GetOptions(aIndex : Integer) : TOptions; overload; function GetSection(aOptionsSection : TOptionsClass; aOptions : TOptions) : Boolean; overload; public constructor Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False); destructor Destroy; override; property FileName : string read fFilename write fFilename; property ReloadIfFileChanged : Boolean read fReloadIfFileChanged write SetReloadIfFileChanged; property IsLoaded : Boolean read fLoaded; property OnFileModified : TFileModifiedEvent read fOnFileModified write fOnFileModified; property OnConfigLoaded : TLoadConfigEvent read fOnConfigLoaded write fOnConfigLoaded; property OnConfigReloaded : TLoadConfigEvent read fOnConfigReloaded write fOnConfigReloaded; property Items[aOptionClass : TOptionsClass] : TOptions read GetOptions; default; property Items[aIndex : Integer] : TOptions read GetOptions; default; function AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions; overload; function AddSection(const aSectionName : string = '') : TOptions; overload; function GetSectionInterface : IOptions; function GetSection(const aSectionName : string = '') : T; overload; function Count : Integer; procedure Load; procedure Save; end; IOptionsBuilder = interface ['{1A1DC9A9-7F2D-4CC4-A772-6C7DBAB34424}'] function Options : T; end; TOptionsBuilder = class(TInterfacedObject,IOptionsBuilder) protected fOptions : T; public constructor Create; function Options : T; end; EOptionConfigureError = class(Exception); EOptionLoadError = class(Exception); EOptionSaveError = class(Exception); EOptionValidationError = class(Exception); implementation { TOptionsContainer } constructor TOptionsContainer.Create(const aFilename : string; aOptionsSerializer : IOptionsSerializer; aReloadIfFileChanged : Boolean = False); begin fSerializer := aOptionsSerializer; fSections := TSectionList.Create(True); fFilename := aFilename; fLoaded := False; fReloadIfFileChanged := aReloadIfFileChanged; if aReloadIfFileChanged then CreateFileMonitor; end; procedure TOptionsContainer.CreateFileMonitor; begin fFileMonitor := TQuickFileMonitor.Create; fFileMonitor.FileName := fFilename; fFileMonitor.Interval := 2000; fFileMonitor.Notifies := [TMonitorNotify.mnFileModified]; fFileMonitor.OnFileChange := FileModifiedNotify; fFileMonitor.Enabled := True; end; destructor TOptionsContainer.Destroy; begin if Assigned(fFileMonitor) then fFileMonitor.Free; fSerializer := nil; fSections.Free; inherited; end; procedure TOptionsContainer.FileModifiedNotify(MonitorNotify: TMonitorNotify); begin if MonitorNotify = TMonitorNotify.mnFileModified then begin if Assigned(fOnFileModified) then fOnFileModified; if fReloadIfFileChanged then begin Load; end; end; end; function TOptionsContainer.AddSection(aOption : TOptionsClass; const aSectionName : string = '') : TOptions; var option : TOptions; begin option := aOption.Create; if aSectionName.IsEmpty then option.Name := Copy(aOption.ClassName,2,aOption.ClassName.Length) else option.Name := aSectionName; fSections.Add(option); Result := option; end; function TOptionsContainer.AddSection(const aSectionName: string): TOptions; var option : TOptions; begin option := TRTTI.CreateInstance; if aSectionName.IsEmpty then option.Name := Copy(T.ClassName,2,T.ClassName.Length) else option.Name := aSectionName; fSections.Add(option); Result.Create(option); end; function TOptionsContainer.Count: Integer; begin Result := fSections.Count; end; function TOptionsContainer.GetOptions(aIndex: Integer): TOptions; begin Result := fSections[aIndex]; end; function TOptionsContainer.GetSection(aOptionsSection: TOptionsClass; aOptions: TOptions): Boolean; var option : TOptions; begin Result := False; for option in fSections do begin if option is TOptionsClass then begin aOptions := option as TOptionsClass; Exit; end; end; end; function TOptionsContainer.GetOptions(aOptionClass : TOptionsClass) : TOptions; var option : TOptions; begin Result := nil; for option in fSections do begin if option is TOptionsClass then Result := option as TOptionsClass; end; end; function TOptionsContainer.GetSection(const aSectionName : string = '') : T; var option : TOptions; begin for option in fSections do begin if option is T then begin if (aSectionName.IsEmpty) or (CompareText(option.Name,aSectionName) = 0) then begin Result := option as T; Exit; end; end; end; end; function TOptionsContainer.GetSectionInterface: IOptions; begin Result := TOptionValue.Create(Self.GetSection); end; procedure TOptionsContainer.Load; var option : TOptions; begin if FileExists(fFilename) then begin fSerializer.Load(fFilename,fSections); if not fLoaded then begin fLoaded := True; if Assigned(fOnConfigLoaded) then fOnConfigLoaded; end else if Assigned(fOnConfigReloaded) then fOnConfigReloaded; end else begin //if not exists file get default values for option in fSections do option.DefaultValues; //creates default file Save; end; end; procedure TOptionsContainer.Save; var laststate : Boolean; begin //disable filemonitor to avoid detect manual save as a external file change laststate := fFileMonitor.Enabled; fFileMonitor.Enabled := False; try //save config file fSerializer.Save(fFilename,fSections); finally //set last state fFileMonitor.Enabled := laststate; end; end; procedure TOptionsContainer.SetReloadIfFileChanged(const Value: Boolean); begin if Value = fReloadIfFileChanged then Exit; fReloadIfFileChanged := Value; if Assigned(fFileMonitor) then fFileMonitor.Free; if fReloadIfFileChanged then CreateFileMonitor; end; { TOptions } function TOptions.ConfigureOptions(aOptionsFunc: TConfigureOptionsProc): IOptionsValidator; var value : TValue; begin Result := Self; if Assigned(aOptionsFunc) then begin value := Self; aOptionsFunc(value.AsType); end; end; constructor TOptions.Create; begin fName := ''; end; procedure TOptions.DoValidateOptions; var ctx : TRttiContext; rtype : TRttiType; rprop : TRttiProperty; attrib : TCustomAttribute; begin ctx := TRttiContext.Create; try rtype := ctx.GetType(Self.ClassInfo); for rprop in rtype.GetProperties do begin //check only published properties if rprop.Visibility = TMemberVisibility.mvPublished then begin //check validation option attributes for attrib in rprop.GetAttributes do begin if attrib is Required then ValidateRequired(rprop) else if attrib is StringLength then ValidateStringLength(rprop,StringLength(attrib)) else if attrib is Range then ValidateRange(rprop,Range(attrib)); end; end; end; finally ctx.Free; end; end; procedure TOptions.ValidateOptions; begin try DoValidateOptions; except on E : Exception do begin raise EOptionConfigureError.CreateFmt('Validation Options Error : %s',[e.Message]); end; end; end; procedure TOptions.ValidateRange(aProperty: TRttiProperty; aValidation : Range); var value : TValue; msg : string; begin value := aProperty.GetValue(Self); if not value.IsEmpty then begin if value.Kind = tkFloat then begin if (value.AsExtended < aValidation.Min) or (value.AsExtended > aValidation.Max) then begin if aValidation.ErrorMsg.IsEmpty then msg := Format('Option "%s.%s" exceeds predefined range (%2f - %2f)',[Self.Name,aProperty.Name,aValidation.Min,aValidation.Max]) else msg := aValidation.ErrorMsg; raise EOptionValidationError.Create(msg); end; end else if value.Kind in [tkInteger,tkInt64] then begin if (value.AsInt64 < aValidation.Min) or (value.AsInt64 > aValidation.Max) then begin 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)]) else msg := aValidation.ErrorMsg; raise EOptionValidationError.Create(msg); end; end; end; end; procedure TOptions.ValidateRequired(aProperty: TRttiProperty); begin if aProperty.GetValue(Self).IsEmpty then raise EOptionValidationError.CreateFmt('Option "%s.%s" is required',[Self.Name,aProperty.Name]); end; procedure TOptions.ValidateStringLength(aProperty: TRttiProperty; aValidation : StringLength); var value : TValue; msg : string; begin value := aProperty.GetValue(Self); if (not value.IsEmpty) and (value.AsString.Length > aValidation.MaxLength) then begin if aValidation.ErrorMsg.IsEmpty then msg := Format('Option "%s.%s" exceeds max length (%d)',[Self.Name,aProperty.Name,aValidation.MaxLength]) else msg := aValidation.ErrorMsg; raise EOptionValidationError.Create(msg); end; end; { Range } constructor Range.Create(aMin, aMax: Integer; const aErrorMsg : string = ''); begin fRangeMin := aMin; fRangeMax := aMax; fErrorMsg := aErrorMsg; end; constructor Range.Create(aMin, aMax: Double; const aErrorMsg: string); begin fRangeMin := aMin; fRangeMax := aMax; fErrorMsg := aErrorMsg; end; { StringLength } constructor StringLength.Create(aMaxLength: Integer; const aErrorMsg : string = ''); begin fMaxLength := aMaxLength; fErrorMsg := aErrorMsg; end; { TOptionValue } constructor TOptionValue.Create(aValue: T); begin fValue := aValue; end; function TOptionValue.GetSectionValue: T; begin Result := fValue; end; { TOptions } function TOptions.ConfigureOptions(aOptionsFunc: TConfigureOptionsProc): IOptionsValidator; begin if Assigned(aOptionsFunc) then Result := fOptions.ConfigureOptions(aOptionsFunc) else Result := fOptions; fOptions._AddRef; end; constructor TOptions.Create(aOptions: T); begin fOptions := aOptions; end; { TOptionsBuilder } constructor TOptionsBuilder.Create; begin fOptions := (PTypeInfo(TypeInfo(T)).TypeData.ClassType.Create) as T; end; function TOptionsBuilder.Options: T; begin Result := fOptions; end; end.