Commits

Anonymous committed fd281e9

Save/Load enums and bools. Skip write if not changed (cascading check).

Comments (0)

Files changed (4)

Src/ASConfig.IniFileEx.pas

   private
     FIni: TCustomIniFile;
     FParentFile: IASConfigFile;
+    FSkipWriteIfNotModified: Boolean;
 
     function GetIniFile: TCustomIniFile;
     function GetParentFile: IASConfigFile;
     procedure MergeStrings(Dest: TStrings; const Src: TStrings);
     function GetFileName: string;
 
+    function GetSkipWriteIfNotModified: Boolean;
+    procedure SetSkipWriteIfNotModified(const Value: Boolean);
+
   protected
     { IInterface }
     FRefCount: Integer;
 
     property IniFile: TCustomIniFile read GetIniFile;
     property ParentFile: IASConfigFile read GetParentFile write SetParentFile;
+    property SkipWriteIfNotModified: Boolean read GetSkipWriteIfNotModified write SetSkipWriteIfNotModified;
   end;
 
 implementation
   Result := IniFile.FileName;
 end;
 
+function TASConfigIniFileEx.GetSkipWriteIfNotModified: Boolean;
+begin
+  Result := FSkipWriteIfNotModified;
+end;
+
 procedure TASConfigIniFileEx.MergeStrings(Dest: TStrings; const Src: TStrings);
 var
   I: Integer;
 
 function TASConfigIniFileEx.ReadString(const Section, Ident, Default: string): string;
 begin
-  Result := IniFile.ReadString(Section, Ident, #1);
-  if Result = #1 then begin
-    if Assigned(ParentFile) then
-      Result := ParentFile.ReadString(Section, Ident, Default)
-    else
-      Result := Default;
+  if Assigned(ParentFile) then
+    Result := ParentFile.ReadString(Section, Ident, Default)
+  else
+    Result := Default;
+
+  Result := IniFile.ReadString(Section, Ident, Result);
+
 {$IFDEF DEBUG_INIFILE}
-  end else begin
     TASDebug.OutputDebugStringFmt('Read String %s/%s = "%s" [%s]'#13#10, [Section, Ident, Result, ToString]);
 {$ENDIF}
-  end;
-
 end;
 
 procedure TASConfigIniFileEx.SetParentFile(const Value: IASConfigFile);
   FParentFile := Value;
 end;
 
+procedure TASConfigIniFileEx.SetSkipWriteIfNotModified(const Value: Boolean);
+begin
+  FSkipWriteIfNotModified := Value;
+end;
+
 function TASConfigIniFileEx.ToString: string;
 begin
   Result := Format('%s(%s) [%s]', [ClassName, FileName, IniFile.FileName]);
   TASDebug.OutputDebugStringFmt('Write String %s/%s = "%s" [%s]'#13#10, [Section, Ident, Value, ToString]);
 {$ENDIF}
 
-  IniFile.WriteString(Section, Ident, Value);
+  if not (SkipWriteIfNotModified and (Value = ReadString(Section, Ident, ''))) then
+    IniFile.WriteString(Section, Ident, Value);
 end;
 
 function TASConfigIniFileEx._AddRef: Integer;
   TLoadPropMethod = function(const Name: string; const Default: TValue): TValue of object;
   TLoadPropMethodMap = array[TTypeKind] of TLoadPropMethod;
 
+  TStoreProperty = TMemberVisibility;
+  TStoreProperties = set of TStoreProperty;
+
   TASCustomConfig = class(TComponent)
   private
     FIniFile: IASConfigFile;
     FParentConfig: TASCustomConfig;
     FSavePropMethodMap: TSavePropMethodMap;
     FLoadPropMethodMap: TLoadPropMethodMap;
+    FStoreProperties: TStoreProperties;
+    FDeclaredOnly: Boolean;
     procedure SetParentConfig(const Value: TASCustomConfig);
+    function GetStorePublic: Boolean;
+    function GetStorePublished: Boolean;
+    procedure SetStorePublic(const Value: Boolean);
+    procedure SetStorePublished(const Value: Boolean);
 
   protected
+    function GetProperties(Typ: TRttiType): TArray<TRttiProperty>; virtual;
     procedure BeforeSave(const Obj: TObject; const Path: string); virtual;
     procedure AfterSave(const Obj: TObject; const Path: string); virtual;
     procedure BeforeLoad(const Obj: TObject; const Path: string); virtual;
 
     property SavePropMethodMap: TSavePropMethodMap read FSavePropMethodMap write FSavePropMethodMap;
     property LoadPropMethodMap: TLoadPropMethodMap read FLoadPropMethodMap write FLoadPropMethodMap;
+
   public
+    constructor Create(AOwner: TComponent); override;
     procedure Save(const Obj: TObject; const Path: string = 'Default');
     procedure Load(const Obj: TObject; const Path: string = 'Default');
     procedure Clear(ClearParents: Boolean = False); virtual;
+
+    property DeclaredOnly: Boolean read FDeclaredOnly write FDeclaredOnly;
     property IniFile: IASConfigFile read FIniFile;
     property ParentConfig: TASCustomConfig read FParentConfig write SetParentConfig;
+    property StoreProperties: TStoreProperties read FStoreProperties write FStoreProperties default [mvPublished];
+    property StorePublic: Boolean read GetStorePublic write SetStorePublic;
+    property StorePublished: Boolean read GetStorePublished write SetStorePublished;
   end;
 
   TASCustomConfigIniFile = class(TASCustomConfig)
   private
     FFileName: string;
     FSection: string;
+    FSkipWriteIfNotModified: Boolean;
 
   protected
     procedure SaveInteger(const Name: string; const Value: TValue);
+    function LoadInteger(const Name: string; const Default: TValue): TValue;
+
     procedure SaveString(const Name: string; const Value: TValue);
+    function LoadString(const Name: string; const Default: TValue): TValue;
+
     procedure SaveFloat(const Name: string; const Value: TValue);
-    function LoadInteger(const Name: string; const Default: TValue): TValue;
-    function LoadString(const Name: string; const Default: TValue): TValue;
     function LoadFloat(const Name: string; const Default: TValue): TValue;
 
+    procedure SaveEnum(const Name: string; const Value: TValue);
+    function LoadEnum(const Name: string; const Default: TValue): TValue;
+
     function CreateIniFile: TCustomIniFile; virtual;
     procedure ClearIniFile; virtual;
     procedure OpenIniFile(const Section: string); virtual;
     constructor Create(AOwner: TComponent); override;
     procedure Clear(ClearParents: Boolean = False); override;
     property FileName: string read FFileName write FFileName;
+    property SkipWriteIfNotModified: Boolean read FSkipWriteIfNotModified write FSkipWriteIfNotModified default True;
   end;
 
   TASCustomConfigRegistryIniFile = class(TASCustomConfigIniFile)
 
   TASConfigIniFile = class(TASCustomConfigIniFile)
   published
+    property DeclaredOnly;
     property FileName;
     property ParentConfig;
+    property SkipWriteIfNotModified;
+    property StoreProperties;
   end;
 
   TASConfigRegistryIniFile = class(TASCustomConfigRegistryIniFile)
     procedure SetRootKeyName(const Value: string);
     function GetRootKeyNameStored: Boolean;
   published
+    property DeclaredOnly;
     property ParentConfig;
     property Path;
     property RootKeyName: string read GetRootKeyName write SetRootKeyName stored GetRootKeyNameStored;
+    property SkipWriteIfNotModified;
+    property StoreProperties;
   end;
 
   TASConfigStore = class(TComponent)
   private
     FConfig: TASCustomConfig;
   public
-    procedure Save(const Obj: TObject; const Path: string = 'Default');
-    procedure Load(const Obj: TObject; const Path: string = 'Default');
+    procedure Save(const Obj: TComponent); overload;
+    procedure Save(const Obj: TObject; const Path: string = 'Default'); overload;
+
+    procedure Load(const Obj: TComponent); overload;
+    procedure Load(const Obj: TObject; const Path: string = 'Default'); overload;
 
   published
     property Config: TASCustomConfig read FConfig write FConfig;
     ParentConfig.Clear(True);
 end;
 
+constructor TASCustomConfig.Create(AOwner: TComponent);
+begin
+  inherited;
+  FStoreProperties := [mvPublished];
+end;
+
+function TASCustomConfig.GetProperties(Typ: TRttiType): TArray<TRttiProperty>;
+begin
+  if DeclaredOnly then
+    Exit(Typ.GetDeclaredProperties);
+
+  Result := Typ.GetProperties;
+end;
+
+function TASCustomConfig.GetStorePublic: Boolean;
+begin
+  Result := mvPublic in StoreProperties;
+end;
+
+function TASCustomConfig.GetStorePublished: Boolean;
+begin
+  Result := mvPublished in StoreProperties;
+end;
+
 procedure TASCustomConfig.Load(const Obj: TObject; const Path: string);
 var
   Ctx: TRttiContext;
   Ctx := TRttiContext.Create;
   try
     T := Ctx.GetType(Obj.ClassType);
-    for P in T.GetProperties do begin
-      if not ((P.Visibility in [mvPublic, mvPublished]) and P.IsReadable) then
+    for P in GetProperties(T) do begin
+      if not ((P.Visibility in StoreProperties) and P.IsWritable) then
         Continue;
 
       L := LoadPropMethodMap[P.PropertyType.TypeKind];
   Ctx := TRttiContext.Create;
   try
     T := Ctx.GetType(Obj.ClassType);
-    for P in T.GetProperties do begin
-      if not ((P.Visibility in [mvPublic, mvPublished]) and P.IsReadable) then
+    for P in GetProperties(T) do begin
+      if not ((P.Visibility in StoreProperties) and P.IsReadable) then
         Continue;
 
       S := SavePropMethodMap[P.PropertyType.TypeKind];
   end;
 end;
 
+procedure TASCustomConfig.SetStorePublic(const Value: Boolean);
+begin
+  if Value then
+    Include(FStoreProperties, mvPublic)
+  else
+    Exclude(FStoreProperties, mvPublic);
+end;
+
+procedure TASCustomConfig.SetStorePublished(const Value: Boolean);
+begin
+  if Value then
+    Include(FStoreProperties, mvPublished)
+  else
+    Exclude(FStoreProperties, mvPublished);
+end;
+
 { TASCustomConfigIniFile }
 
 procedure TASCustomConfigIniFile.AfterLoad(const Obj: TObject; const Path: string);
 begin
   inherited Create(AOwner);
 
+  FSkipWriteIfNotModified := True;
+
   SaveMap[tkUnknown     ] := nil;
   SaveMap[tkInteger     ] := SaveInteger;
   SaveMap[tkChar        ] := nil;
-  SaveMap[tkEnumeration ] := nil;
+  SaveMap[tkEnumeration ] := SaveEnum;
   SaveMap[tkFloat       ] := SaveFloat;
   SaveMap[tkString      ] := SaveString;
   SaveMap[tkSet         ] := nil;
   LoadMap[tkUnknown     ] := nil;
   LoadMap[tkInteger     ] := LoadInteger;
   LoadMap[tkChar        ] := nil;
-  LoadMap[tkEnumeration ] := nil;
+  LoadMap[tkEnumeration ] := LoadEnum;
   LoadMap[tkFloat       ] := LoadFloat;
   LoadMap[tkString      ] := LoadString;
   LoadMap[tkSet         ] := nil;
   Result := TIniFile.Create(FileName);
 end;
 
+function TASCustomConfigIniFile.LoadEnum(const Name: string; const Default: TValue): TValue;
+var
+  I: Integer;
+begin
+  if Default.TypeInfo^.Name = 'Boolean' then
+    Exit(TValue.From(IniFile.ReadBool(Section, Name, Default.AsBoolean)));
+
+  I := 0;
+  Default.ExtractRawData(@I);
+  I := GetEnumValue(
+    Default.TypeInfo,
+    IniFile.ReadString(Section, Name, GetEnumName(Default.TypeInfo, I))
+  );
+
+  TValue.Make(@I, Default.TypeInfo, Result);
+end;
+
 function TASCustomConfigIniFile.LoadFloat(const Name: string; const Default: TValue): TValue;
 begin
   Result := TValue.From(IniFile.ReadFloat(Section, Name, Default.AsExtended));
 
   Self.Section := Section;
   FIniFile := TASConfigIniFileEx.Create(Name, CreateIniFile, Parent);
+  IniFile.SkipWriteIfNotModified := SkipWriteIfNotModified;
+end;
+
+procedure TASCustomConfigIniFile.SaveEnum(const Name: string; const Value: TValue);
+var
+  I: Integer;
+begin
+  if Value.TypeInfo^.Name = 'Boolean' then
+    IniFile.WriteBool(Section, Name, Value.AsBoolean)
+  else begin
+    I := 0;
+    Value.ExtractRawData(@I);
+    IniFile.WriteString(Section, Name, GetEnumName(Value.TypeInfo, I));
+  end;
 end;
 
 procedure TASCustomConfigIniFile.SaveFloat(const Name: string; const Value: TValue);
   Config.Load(Obj, Path);
 end;
 
+procedure TASConfigStore.Load(const Obj: TComponent);
+begin
+  Load(Obj, Obj.Name);
+end;
+
 procedure TASConfigStore.Save(const Obj: TObject; const Path: string);
 begin
   Config.Save(Obj, Path);
     (IniFile.IniFile as TRegistryIniFile).RegIniFile.RootKey := Value;
 end;
 
+procedure TASConfigStore.Save(const Obj: TComponent);
+begin
+  Save(Obj, Obj.Name);
+end;
+
 { TASConfigRegistryIniFile }
 
 function TASConfigRegistryIniFile.GetRootKeyName: string;

Src/ASConfigInterface.pas

     procedure SetParentFile(const Value: IASConfigFile);
     property ParentFile: IASConfigFile read GetParentFile write SetParentFile;
 
+    function GetSkipWriteIfNotModified: Boolean;
+    procedure SetSkipWriteIfNotModified(const Value: Boolean);
+    property SkipWriteIfNotModified: Boolean read GetSkipWriteIfNotModified write SetSkipWriteIfNotModified;
+
     { Public interface of TCustomIniFile }
     function SectionExists(const Section: string): Boolean;
     function ReadString(const Section, Ident, Default: string): string;

Test/TestASConfig.pas

   published
     procedure TestSaveToIniFile;
     procedure TestLoadFromIniFile;
+    procedure TestDeclaredOnly;
+    procedure TestPublishedOnly;
+    procedure TestPublicOnly;
+    procedure TestSaveBool;
+    procedure TestSaveEnum;
+    procedure TestLoadEnum;
   end;
 
   TestTASConfigIniFile = class(TestTASCustomConfig)
     property Aint: Integer read Fint write Fint;
   end;
 
+  TMyEnum = (meOther, meFoo, meTest, meBar, meLast);
+  TTestDescendent = class(TTestObject)
+  private
+    FAnotherProp: Boolean;
+    FMyEnum: TMyEnum;
+  public
+    property MyEnum: TMyEnum read FMyEnum write FMyEnum;
+  published
+    property AnotherProp: Boolean read FAnotherProp write FAnotherProp;
+  end;
+
 implementation
 
 uses
   FreeAndNil(FASCustomConfig);
 end;
 
+procedure TestTASCustomConfig.TestDeclaredOnly;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Ini := CreateIniFile;
+  Ini.WriteString('Default', 'Astring', 'My string');
+  Ini.WriteInteger('Default', 'Aint', 44);
+  Ini.WriteBool('Default', 'AnotherProp', True);
+  Ini := nil;
+
+  Obj := TTestDescendent.Create;
+  try
+    Config.DeclaredOnly := True;
+    Config.StorePublic := True;
+    Config.StorePublished := True;
+
+    Config.Load(Obj);
+
+    CheckEqualsString('', Obj.Astring, '[Default]');
+    CheckEquals(0, Obj.Aint, '[Default]');
+    CheckEquals(True, Obj.AnotherProp, '[Default]');
+  finally
+    Obj.Free;
+  end;
+end;
+
+procedure TestTASCustomConfig.TestLoadEnum;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Ini := CreateIniFile;
+  Ini.WriteString('Default', 'MyEnum', 'meFoo');
+  Ini := nil;
+
+  Obj := TTestDescendent.Create;
+  try
+    Config.DeclaredOnly := True;
+    Config.StorePublic := True;
+
+    Config.Load(Obj);
+
+    Check(Obj.MyEnum = meFoo, '[Default] MyEnum = meFoo');
+  finally
+    Obj.Free;
+  end;
+end;
+
 procedure TestTASCustomConfig.TestLoadFromIniFile;
 var
   Obj: TTestObject;
 
   Obj := TTestObject.Create;
   try
+    Config.StorePublic := True;
     Config.Load(Obj);
 
     CheckEqualsString('My string', Obj.Astring, '[Default]');
   end;
 end;
 
+procedure TestTASCustomConfig.TestPublicOnly;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Ini := CreateIniFile;
+  Ini.WriteString('Default', 'Astring', 'My string');
+  Ini.WriteInteger('Default', 'Aint', 44);
+  Ini.WriteBool('Default', 'AnotherProp', True);
+  Ini := nil;
+
+  Obj := TTestDescendent.Create;
+  try
+    Config.DeclaredOnly := False;
+    Config.StoreProperties := [TStoreProperty.mvPublic];
+
+    Config.Load(Obj);
+
+    CheckEqualsString('My string', Obj.Astring, '[Default]');
+    CheckEquals(44, Obj.Aint, '[Default]');
+    CheckEquals(False, Obj.AnotherProp, '[Default]');
+  finally
+    Obj.Free;
+  end;
+end;
+
+procedure TestTASCustomConfig.TestPublishedOnly;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Ini := CreateIniFile;
+  Ini.WriteString('Default', 'Astring', 'My string');
+  Ini.WriteInteger('Default', 'Aint', 44);
+  Ini.WriteBool('Default', 'AnotherProp', True);
+  Ini := nil;
+
+  Obj := TTestDescendent.Create;
+  try
+    Config.DeclaredOnly := False;
+    Config.StoreProperties := [];
+    Config.StorePublished := True;
+
+    Config.Load(Obj);
+
+    CheckEqualsString('', Obj.Astring, '[Default]');
+    CheckEquals(0, Obj.Aint, '[Default]');
+    CheckEquals(True, Obj.AnotherProp, '[Default]');
+  finally
+    Obj.Free;
+  end;
+end;
+
+procedure TestTASCustomConfig.TestSaveBool;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Obj := TTestDescendent.Create;
+  try
+    Obj.AnotherProp := True;
+    Config.DeclaredOnly := True;
+    Config.Save(Obj);
+  finally
+    Obj.Free;
+  end;
+
+  Ini := CreateIniFile;
+  CheckTrue(Ini.ReadBool('Default', 'AnotherProp', False));
+end;
+
+procedure TestTASCustomConfig.TestSaveEnum;
+var
+  Obj: TTestDescendent;
+  Ini: IASConfigFile;
+begin
+  Config.Clear;
+
+  Obj := TTestDescendent.Create;
+  try
+    Obj.MyEnum := meBar;
+    Config.DeclaredOnly := True;
+    Config.StorePublic := True;
+    Config.Save(Obj);
+  finally
+    Obj.Free;
+  end;
+
+  Ini := CreateIniFile;
+  CheckEqualsString('meBar', Ini.ReadString('Default', 'MyEnum', ''));
+end;
+
 procedure TestTASCustomConfig.TestSaveToIniFile;
 var
   Obj: TTestObject;
     Obj.Astring := 'A string';
     Obj.Aint := 55;
 
+    Config.StorePublic := True;
     Config.Save(Obj);
 
   finally
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.