Proof of concept

This commit is contained in:
Mark van Renswoude 2020-09-09 11:05:09 +02:00
parent d050b7bb1c
commit f0aed6f1e4
10 changed files with 1839 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__history
Win32
*.local
*.identcache
*.res

48
DaintyGroupDXE2.groupproj Normal file
View File

@ -0,0 +1,48 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{CE176D45-2767-4150-B737-D72F170B25C3}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Projects Include="UnitTests\DaintyUnitTests.dproj">
<Dependencies/>
</Projects>
<Projects Include="Packages\DaintyDXE2.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Default.Personality/>
</BorlandProject>
</ProjectExtensions>
<Target Name="DaintyUnitTests">
<MSBuild Projects="UnitTests\DaintyUnitTests.dproj"/>
</Target>
<Target Name="DaintyUnitTests:Clean">
<MSBuild Projects="UnitTests\DaintyUnitTests.dproj" Targets="Clean"/>
</Target>
<Target Name="DaintyUnitTests:Make">
<MSBuild Projects="UnitTests\DaintyUnitTests.dproj" Targets="Make"/>
</Target>
<Target Name="DaintyDXE2">
<MSBuild Projects="Packages\DaintyDXE2.dproj"/>
</Target>
<Target Name="DaintyDXE2:Clean">
<MSBuild Projects="Packages\DaintyDXE2.dproj" Targets="Clean"/>
</Target>
<Target Name="DaintyDXE2:Make">
<MSBuild Projects="Packages\DaintyDXE2.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="DaintyUnitTests;DaintyDXE2"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="DaintyUnitTests:Clean;DaintyDXE2:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="DaintyUnitTests:Make;DaintyDXE2:Make"/>
</Target>
<Import Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')" Project="$(BDS)\Bin\CodeGear.Group.Targets"/>
</Project>

39
Packages/DaintyDXE2.dpk Normal file
View File

@ -0,0 +1,39 @@
package DaintyDXE2;
{$R *.res}
{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO ON}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION OFF}
{$OVERFLOWCHECKS OFF}
{$RANGECHECKS OFF}
{$REFERENCEINFO ON}
{$SAFEDIVIDE OFF}
{$STACKFRAMES ON}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST OFF}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DEFINE DEBUG}
{$ENDIF IMPLICITBUILDING}
{$RUNONLY}
{$IMPLICITBUILD ON}
requires
rtl,
dbrtl;
contains
Dainty in '..\Source\Dainty.pas',
Dainty.ValueSetter.Default in '..\Source\Dainty.ValueSetter.Default.pas';
end.

140
Packages/DaintyDXE2.dproj Normal file
View File

@ -0,0 +1,140 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{EFB63B96-967A-4C05-BD7C-50D48E7E8E43}</ProjectGuid>
<MainSource>DaintyDXE2.dpk</MainSource>
<ProjectVersion>13.4</ProjectVersion>
<FrameworkType>None</FrameworkType>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Package</AppType>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
<Cfg_1_Win32>true</Cfg_1_Win32>
<CfgParent>Cfg_1</CfgParent>
<Cfg_1>true</Cfg_1>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<RuntimeOnlyPackage>true</RuntimeOnlyPackage>
<VerInfo_Locale>1043</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
<DCC_BplOutput>$(DELPHIBIN)</DCC_BplOutput>
<DCC_DcpOutput>$(DELPHIBIN)</DCC_DcpOutput>
<DCC_CBuilderOutput>All</DCC_CBuilderOutput>
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace)</DCC_Namespace>
<GenPackage>true</GenPackage>
<GenDll>true</GenDll>
<DCC_DcuOutput>$(DELPHILIB)</DCC_DcuOutput>
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
<VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
<DCC_Optimize>false</DCC_Optimize>
<DCC_GenerateStackFrames>true</DCC_GenerateStackFrames>
<DCC_DebugInfoInExe>true</DCC_DebugInfoInExe>
<DCC_RemoteDebug>true</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_RemoteDebug>false</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="rtl.dcp"/>
<DCCReference Include="dbrtl.dcp"/>
<DCCReference Include="..\Source\Dainty.pas"/>
<DCCReference Include="..\Source\Dainty.ValueSetter.Default.pas"/>
<BuildConfiguration Include="Release">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType>Package</Borland.ProjectType>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">DaintyDXE2.dpk</Source>
</Source>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">True</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">1</VersionInfo>
<VersionInfo Name="MinorVer">0</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1043</VersionInfo>
<VersionInfo Name="CodePage">1252</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName"/>
<VersionInfoKeys Name="FileDescription"/>
<VersionInfoKeys Name="FileVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName"/>
<VersionInfoKeys Name="LegalCopyright"/>
<VersionInfoKeys Name="LegalTrademarks"/>
<VersionInfoKeys Name="OriginalFilename"/>
<VersionInfoKeys Name="ProductName"/>
<VersionInfoKeys Name="ProductVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments"/>
</VersionInfoKeys>
</Delphi.Personality>
<Deployment/>
<Platforms>
<Platform value="Win64">False</Platform>
<Platform value="Win32">True</Platform>
</Platforms>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')" Project="$(BDS)\Bin\CodeGear.Delphi.Targets"/>
<Import Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')" Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj"/>
</Project>

View File

@ -0,0 +1,118 @@
{
Dainty
Simple object mapper for Delphi
Copyright (c) 2020 M. van Renswoude
https://github.com/MvRens/Dainty
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
}
unit Dainty.ValueSetter.Default;
interface
implementation
uses
Data.DB,
System.Rtti,
System.TypInfo,
Dainty;
type
TDaintyDefaultValueSetterFactory = class(TDaintyAbstractValueSetterFactory)
public
class function Construct(AMember: TDaintyRttiMember): TDaintyValueSetter; override;
end;
{ TDaintyDefaultValueSetterFactory }
class function TDaintyDefaultValueSetterFactory.Construct(AMember: TDaintyRttiMember): TDaintyValueSetter;
begin
Result := nil;
case AMember.RttiType.TypeKind of
tkString,
tkLString,
tkWString,
tkUString,
tkChar,
tkWChar:
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsString);
end;
tkInteger:
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsInteger);
end;
tkEnumeration:
if AMember.RttiType.Handle = TypeInfo(Boolean) then
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsBoolean);
end
else
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsInteger);
end;
tkInt64:
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsLargeInt);
end;
tkFloat:
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, AField.AsFloat);
end;
tkVariant:
Result :=
procedure(AInstance: TObject; AField: TField)
begin
AMember.SetValue(AInstance, TValue.FromVariant(AField.Value));
end;
end;
end;
initialization
TDaintyRttiMapperFactory.RegisterValueSetterFactory(TDaintyDefaultValueSetterFactory);
end.

845
Source/Dainty.pas Normal file
View File

@ -0,0 +1,845 @@
{
Dainty
Simple object mapper for Delphi
Copyright (c) 2020 M. van Renswoude
https://github.com/MvRens/Dainty
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
}
unit Dainty;
interface
uses
Data.DB,
System.Generics.Collections,
System.Rtti,
System.SysUtils,
Dainty.ValueSetter.Default;
type
TDaintyMapper<T: class> = class;
/// <summary>
/// Annotates a field or property to change it's field name in the DataSet
/// as used for the Dainty methods.
/// </summary>
FieldName = class(TCustomAttribute)
private
FFieldName: string;
public
constructor Create(const AFieldName: string);
property FieldName: string read FFieldName;
end;
/// <summary>
/// Allows for direct calls to TDainty methods from any DataSet instance, for example
/// DataSet.Rows<> or DataSet.GetFirstOrDefault<>.
/// </summary>
/// <remarks>
/// Because class helpers are not exactly extension methods and only one can apply,
/// if you're having conflicts you can call TDainty.Rows<>(DataSet) instead.
/// </remarks>
TDaintyDataSetHelper = class helper for TDataSet
public
/// <summary>
/// Returns a typed enumerable which iterates the DataSet and returns the mapped
/// object for each row.
/// </summary>
/// <remarks>
/// Note that the DataSet is not reset to First and will instead start at the current record.
/// </remarks>
function Rows<T: class>: TEnumerable<T>;
/// <summary>
/// Provides access to the mapper which allows control over the DataSet loop.
/// </summary>
function GetMapper<T: class>: TDaintyMapper<T>;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active.
/// </summary>
function GetFirst<T: class>: T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active.
/// </summary>
function GetFirstOrDefault<T: class>(const ADefault: T): T;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active or if more than one row is remaining.
/// </summary>
function GetSingle<T: class>: T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active or if more than one row is remaining.
/// </summary>
function GetSingleOrDefault<T: class>(const ADefault: T): T;
end;
/// <summary>
/// Provides row to object mapping functionality. Usually accessed using the TDaintyDataSetHelper.
/// </summary>
TDainty = class
public
/// <summary>
/// Returns a typed enumerable which iterates the DataSet and returns the mapped
/// object for each row.
/// </summary>
/// <remarks>
/// Note that the DataSet is not reset to First and will instead start at the current record.
/// </remarks>
class function Rows<T: class>(ADataSet: TDataSet): TEnumerable<T>;
/// <summary>
/// Provides access to the mapper which allows control over the DataSet loop.
/// </summary>
class function GetMapper<T: class>(ADataSet: TDataSet): TDaintyMapper<T>;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active.
/// </summary>
class function GetFirst<T: class>(ADataSet: TDataSet): T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active.
/// </summary>
class function GetFirstOrDefault<T: class>(ADataSet: TDataSet; const ADefault: T): T;
/// <summary>
/// Returns the current row mapped to the specified class. Throws an exception if no
/// row is active or if more than one row is remaining.
/// </summary>
class function GetSingle<T: class>(ADataSet: TDataSet): T;
/// <summary>
/// Returns the current row mapped to the specified class. Returns the value of
/// ADefault if no row is active or if more than one row is remaining.
/// </summary>
class function GetSingleOrDefault<T: class>(ADataSet: TDataSet; const ADefault: T): T;
end;
/// <summary>
/// Performs the mapping of the current row to the specified type.
/// </summary>
TDaintyMapper<T: class> = class
public
function MapRow: T; virtual; abstract;
end;
/// <summary>
/// Enumerates over the rows of a DataSet and returns the mapped objects.
/// Returned by the Rows<> method.
/// </summary>
TDaintyEnumerable<T: class> = class(TEnumerable<T>)
private
FMapper: TDaintyMapper<T>;
FDataSet: TDataSet;
protected
function DoGetEnumerator: TEnumerator<T>; override;
public
constructor Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
end;
/// <summary>
/// Internal enumerator returned by TDaintyEnumerable.
/// </summary>
/// <remarks>
/// For internal use only. It's only in the interface section to prevent "Method of parameterized type declared in interface section must not use local symbol" error.
/// </remarks>
TDaintyEnumerator<T: class> = class(TEnumerator<T>)
private
FMapper: TDaintyMapper<T>;
FDataSet: TDataSet;
FCurrent: T;
protected
function DoGetCurrent: T; override;
function DoMoveNext: Boolean; override;
public
constructor Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
end;
TDaintyValueSetter = reference to procedure(AInstance: TObject; AField: TField);
TDaintyRttiFieldNameMapping = record
FieldName: string;
ValueSetter: TDaintyValueSetter;
constructor Create(const AFieldName: string; const AValueSetter: TDaintyValueSetter);
end;
TDaintyRttiClassMapping = class
private
FConstruct: TFunc<TObject>;
FFieldNameMapping: TList<TDaintyRttiFieldNameMapping>;
public
constructor Create(AConstruct: TFunc<TObject>);
destructor Destroy; override;
property Construct: TFunc<TObject> read FConstruct;
property FieldNameMapping: TList<TDaintyRttiFieldNameMapping> read FFieldNameMapping;
end;
TDaintyRttiFieldMapping = record
Field: TField;
ValueSetter: TDaintyValueSetter;
constructor Create(const AField: TField; const AValueSetter: TDaintyValueSetter);
end;
TDaintyRttiMapper<T: class> = class(TDaintyMapper<T>)
private
FConstruct: TFunc<TObject>;
FFieldMapping: TList<TDaintyRttiFieldMapping>;
public
constructor Create(AClassMapping: TDaintyRttiClassMapping; ADataSet: TDataSet);
function MapRow: T; override;
end;
TDaintyRttiMember = class
private
FRttiMember: TRttiMember;
protected
function GetRttiType: TRttiType; virtual; abstract;
public
constructor Create(ARttiMember: TRttiMember);
procedure SetValue(AInstance: TObject; const AValue: TValue); virtual; abstract;
property RttiMember: TRttiMember read FRttiMember;
property RttiType: TRttiType read GetRttiType;
end;
TDaintyRttiFieldMember = class(TDaintyRttiMember)
private
FField: TRttiField;
protected
function GetRttiType: TRttiType; override;
public
constructor Create(AField: TRttiField);
procedure SetValue(AInstance: TObject; const AValue: TValue); override;
end;
TDaintyRttiPropertyMember = class(TDaintyRttiMember)
private
FProperty: TRttiProperty;
protected
function GetRttiType: TRttiType; override;
public
constructor Create(AProperty: TRttiProperty);
procedure SetValue(AInstance: TObject; const AValue: TValue); override;
end;
TDaintyAbstractValueSetterFactoryClass = class of TDaintyAbstractValueSetterFactory;
TDaintyAbstractValueSetterFactory = class
public
class function Construct(AMember: TDaintyRttiMember): TDaintyValueSetter; virtual; abstract;
end;
TDaintyRttiMapperFactory = class
private type
TValueSetterRegistration = record
Factory: TDaintyAbstractValueSetterFactoryClass;
Priority: Integer;
constructor Create(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
end;
private class var
SContext: TRttiContext;
SClassMappingCacheLock: TMultiReadExclusiveWriteSynchronizer;
SClassMappingCache: TDictionary<Pointer, TDaintyRttiClassMapping>;
SValueSettersLock: TMultiReadExclusiveWriteSynchronizer;
SValueSetters: TList<TValueSetterRegistration>;
SMembers: TObjectList<TDaintyRttiMember>;
private
class function GetClassMapping<T: class>: TDaintyRttiClassMapping;
class function GetFieldName(AMember: TRttiNamedObject): string;
class function GetValueSetter(AMember: TRttiMember): TDaintyValueSetter;
protected
class procedure Initialize;
class procedure Finalize;
public
class function Construct<T: class>(ADataSet: TDataSet): TDaintyRttiMapper<T>;
class procedure RegisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer = 0);
class procedure UnregisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass);
end;
implementation
uses
System.TypInfo;
{ TDaintyDataSetHelper }
function TDaintyDataSetHelper.Rows<T>: TEnumerable<T>;
begin
Result := TDainty.Rows<T>(Self);
end;
function TDaintyDataSetHelper.GetMapper<T>: TDaintyMapper<T>;
begin
Result := TDainty.GetMapper<T>(Self);
end;
function TDaintyDataSetHelper.GetFirst<T>: T;
begin
Result := TDainty.GetFirst<T>(Self);
end;
function TDaintyDataSetHelper.GetFirstOrDefault<T>(const ADefault: T): T;
begin
Result := TDainty.GetFirstOrDefault<T>(Self, ADefault);
end;
function TDaintyDataSetHelper.GetSingle<T>: T;
begin
Result := TDainty.GetSingle<T>(Self);
end;
function TDaintyDataSetHelper.GetSingleOrDefault<T>(const ADefault: T): T;
begin
Result := TDainty.GetSingleOrDefault<T>(Self, ADefault);
end;
{ TDainty }
class function TDainty.Rows<T>(ADataSet: TDataSet): TEnumerable<T>;
var
mapper: TDaintyMapper<T>;
begin
mapper := GetMapper<T>(ADataSet);
Result := TDaintyEnumerable<T>.Create(mapper, ADataSet);
end;
class function TDainty.GetMapper<T>(ADataSet: TDataSet): TDaintyMapper<T>;
begin
Result := TDaintyRttiMapperFactory.Construct<T>(ADataSet);
end;
class function TDainty.GetFirst<T>(ADataSet: TDataSet): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
raise EDatabaseError.Create('Expected at least 1 record but none found');
Result := enumerator.Current;
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetFirstOrDefault<T>(ADataSet: TDataSet; const ADefault: T): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if enumerator.MoveNext then
Result := enumerator.Current
else
Result := ADefault;
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetSingle<T>(ADataSet: TDataSet): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
raise EDatabaseError.Create('Expected 1 record but none found');
Result := enumerator.Current;
if enumerator.MoveNext then
raise EDatabaseError.Create('Expected 1 record but more found');
finally
FreeAndNil(enumerator);
end;
end;
class function TDainty.GetSingleOrDefault<T>(ADataSet: TDataSet; const ADefault: T): T;
var
enumerator: TEnumerator<T>;
begin
enumerator := Rows<T>(ADataSet).GetEnumerator;
try
if not enumerator.MoveNext then
Exit(ADefault);
Result := enumerator.Current;
if enumerator.MoveNext then
Exit(ADefault);
finally
FreeAndNil(enumerator);
end;
end;
{ TDaintyEnumerable<T> }
constructor TDaintyEnumerable<T>.Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
begin
inherited Create;
FMapper := AMapper;
FDataSet := ADataSet;
end;
function TDaintyEnumerable<T>.DoGetEnumerator: TEnumerator<T>;
begin
Result := TDaintyEnumerator<T>.Create(FMapper, FDataSet);
end;
{ TDaintyEnumerator<T> }
constructor TDaintyEnumerator<T>.Create(AMapper: TDaintyMapper<T>; ADataSet: TDataSet);
begin
inherited Create;
FMapper := AMapper;
FDataSet := ADataSet;
end;
function TDaintyEnumerator<T>.DoGetCurrent: T;
begin
Result := FCurrent;
end;
function TDaintyEnumerator<T>.DoMoveNext: Boolean;
begin
if FDataSet.Eof then
Exit(False);
FCurrent := FMapper.MapRow;
Result := True;
FDataSet.Next;
end;
{ TDaintyRttiMapper<T> }
constructor TDaintyRttiMapper<T>.Create(AClassMapping: TDaintyRttiClassMapping; ADataSet: TDataSet);
var
fieldNameMapping: TDaintyRttiFieldNameMapping;
begin
inherited Create;
FConstruct := AClassMapping.Construct;
FFieldMapping := TList<TDaintyRttiFieldMapping>.Create;
for fieldNameMapping in AClassMapping.FieldNameMapping do
FFieldMapping.Add(TDaintyRttiFieldMapping.Create(ADataSet.FieldByName(fieldNameMapping.FieldName), fieldNameMapping.ValueSetter));
end;
{ TDaintyRttiMember }
constructor TDaintyRttiMember.Create(ARttiMember: TRttiMember);
begin
inherited Create;
FRttiMember := ARttiMember;
end;
{ TDaintyRttiFieldMember }
constructor TDaintyRttiFieldMember.Create(AField: TRttiField);
begin
inherited Create(AField);
FField := AField;
end;
function TDaintyRttiFieldMember.GetRttiType: TRttiType;
begin
Result := FField.FieldType;
end;
procedure TDaintyRttiFieldMember.SetValue(AInstance: TObject; const AValue: TValue);
begin
FField.SetValue(AInstance, AValue);
end;
{ TDaintyRttiPropertyMember }
constructor TDaintyRttiPropertyMember.Create(AProperty: TRttiProperty);
begin
inherited Create(AProperty);
FProperty := AProperty;
end;
function TDaintyRttiPropertyMember.GetRttiType: TRttiType;
begin
Result := FProperty.PropertyType;
end;
procedure TDaintyRttiPropertyMember.SetValue(AInstance: TObject; const AValue: TValue);
begin
FProperty.SetValue(AInstance, AValue);
end;
{ TDaintyRttiMapperFactory }
class procedure TDaintyRttiMapperFactory.Initialize;
begin
if Assigned(SClassMappingCache) then
Exit;
SContext := TRttiContext.Create;
SClassMappingCacheLock := TMultiReadExclusiveWriteSynchronizer.Create;
SClassMappingCache := TObjectDictionary<Pointer, TDaintyRttiClassMapping>.Create([doOwnsValues]);
SValueSettersLock := TMultiReadExclusiveWriteSynchronizer.Create;
SValueSetters := TList<TValueSetterRegistration>.Create;
SMembers := TObjectList<TDaintyRttiMember>.Create;
end;
class procedure TDaintyRttiMapperFactory.RegisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
var
registration: TValueSetterRegistration;
registrationIndex: Integer;
begin
{ Initialization for Dainty.ValueSetter.Default runs before ours, make sure we are initialized }
Initialize;
SValueSettersLock.BeginWrite;
try
registration := TValueSetterRegistration.Create(AFactory, APriority);
for registrationIndex := 0 to Pred(SValueSetters.Count) do
if SValueSetters[registrationIndex].Priority <= APriority then
begin
SValueSetters.Insert(registrationIndex, registration);
Exit;
end;
SValueSetters.Add(registration);
finally
SValueSettersLock.EndWrite;
end;
end;
class procedure TDaintyRttiMapperFactory.UnregisterValueSetterFactory(AFactory: TDaintyAbstractValueSetterFactoryClass);
var
registrationIndex: Integer;
begin
SValueSettersLock.BeginWrite;
try
for registrationIndex := Pred(SValueSetters.Count) downto 0 do
if SValueSetters[registrationIndex].Factory = AFactory then
SValueSetters.Delete(registrationIndex);
finally
SValueSettersLock.EndWrite;
end;
end;
class procedure TDaintyRttiMapperFactory.Finalize;
begin
FreeAndNil(SValueSetters);
FreeAndNil(SValueSettersLock);
FreeAndNil(SClassMappingCache);
FreeAndNil(SClassMappingCacheLock);
SContext.Free;
end;
class function TDaintyRttiMapperFactory.Construct<T>(ADataSet: TDataSet): TDaintyRttiMapper<T>;
var
mapping: TDaintyRttiClassMapping;
begin
mapping := GetClassMapping<T>;
Result := TDaintyRttiMapper<T>.Create(mapping, ADataSet);
end;
class function TDaintyRttiMapperFactory.GetClassMapping<T>: TDaintyRttiClassMapping;
var
typeInfoHandle: Pointer;
classInfo: TRttiType;
method: TRttiMethod;
instanceClassType: TClass;
fieldInfo: TRttiField;
propertyInfo: TRttiProperty;
valueSetter: TProc<TObject, TField>;
begin
SClassMappingCacheLock.BeginRead;
try
typeInfoHandle := TypeInfo(T);
if SClassMappingCache.TryGetValue(typeInfoHandle, Result) then
Exit;
SClassMappingCacheLock.BeginWrite;
try
{ Between the call to BeginWrite and actually acquiring the lock the state
may have changed. Check again to be sure. }
if SClassMappingCache.TryGetValue(typeInfoHandle, Result) then
Exit;
classInfo := SContext.GetType(typeInfoHandle);
Result := nil;
for method in classInfo.GetMethods do
begin
if method.IsConstructor and (Length(method.GetParameters) = 0) then
begin
instanceClassType := classInfo.AsInstance.MetaclassType;
Result := TDaintyRttiClassMapping.Create(
function: TObject
begin
Result := method.Invoke(instanceClassType, []).AsObject;
end);
Break;
end;
end;
if not Assigned(Result) then
raise ENoConstructException.Create('A constructor with no parameter is required for Dainty');
for fieldInfo in classInfo.GetFields do
begin
if not (fieldInfo.Visibility in [mvPublic, mvPublished]) then
Continue;
Result.FieldNameMapping.Add(TDaintyRttiFieldNameMapping.Create(GetFieldName(fieldInfo), GetValueSetter(fieldInfo)));
end;
for propertyInfo in classInfo.GetProperties do
begin
if not (propertyInfo.Visibility in [mvPublic, mvPublished]) then
Continue;
if not propertyInfo.IsWritable then
Continue;
Result.FieldNameMapping.Add(TDaintyRttiFieldNameMapping.Create(GetFieldName(propertyInfo), GetValueSetter(propertyInfo)));
end;
SClassMappingCache.Add(typeInfoHandle, Result);
finally
SClassMappingCacheLock.EndWrite;
end;
finally
SClassMappingCacheLock.EndRead;
end;
end;
class function TDaintyRttiMapperFactory.GetFieldName(AMember: TRttiNamedObject): string;
var
attribute: TCustomAttribute;
begin
for attribute in AMember.GetAttributes do
begin
if attribute is FieldName then
Exit(FieldName(attribute).FieldName);
end;
Result := AMember.Name;
end;
class function TDaintyRttiMapperFactory.GetValueSetter(AMember: TRttiMember): TDaintyValueSetter;
var
member: TDaintyRttiMember;
registration: TValueSetterRegistration;
begin
if AMember is TRttiField then
member := TDaintyRttiFieldMember.Create(TRttiField(AMember))
else if AMember is TRttiProperty then
member := TDaintyRttiPropertyMember.Create(TRttiProperty(AMember))
else
raise EInvalidOpException.CreateFmt('Member type not supported: %s', [AMember.ClassName]);
SValueSettersLock.BeginRead;
try
for registration in SValueSetters do
begin
Result := registration.Factory.Construct(member);
if Assigned(Result) then
begin
SMembers.Add(member);
Break;
end;
end;
finally
SValueSettersLock.EndRead;
end;
if not Assigned(Result) then
begin
FreeAndNil(member);
raise ENotSupportedException.CreateFmt('Member %s has unsupported type for Dainty: %d', [AMember.Name, Ord(member.RttiType.TypeKind)]);
end;
end;
{ TDaintyRttiMapperFactory.TValueSetterRegistration }
constructor TDaintyRttiMapperFactory.TValueSetterRegistration.Create(AFactory: TDaintyAbstractValueSetterFactoryClass; APriority: Integer);
begin
Factory := AFactory;
Priority := APriority;
end;
{ TDaintyRttiMapper<T> }
function TDaintyRttiMapper<T>.MapRow: T;
var
fieldMapping: TDaintyRttiFieldMapping;
begin
Result := FConstruct() as T;
for fieldMapping in FFieldMapping do
fieldMapping.ValueSetter(Result, fieldMapping.Field);
end;
{ TDaintyRttiFieldNameMapping }
constructor TDaintyRttiFieldNameMapping.Create(const AFieldName: string; const AValueSetter: TDaintyValueSetter);
begin
FieldName := AFieldName;
ValueSetter := AValueSetter;
end;
{ TDaintyRttiFieldMapping }
constructor TDaintyRttiFieldMapping.Create(const AField: TField; const AValueSetter: TDaintyValueSetter);
begin
Field := AField;
ValueSetter := AValueSetter;
end;
{ TDaintyRttiClassMapping }
constructor TDaintyRttiClassMapping.Create(AConstruct: TFunc<TObject>);
begin
inherited Create;
FConstruct := AConstruct;
FFieldNameMapping := TList<TDaintyRttiFieldNameMapping>.Create;
end;
destructor TDaintyRttiClassMapping.Destroy;
begin
FreeAndNil(FFieldNameMapping);
inherited Destroy;
end;
{ FieldName }
constructor FieldName.Create(const AFieldName: string);
begin
inherited Create;
FFieldName := AFieldName;
end;
initialization
TDaintyRttiMapperFactory.Initialize;
finalization
TDaintyRttiMapperFactory.Finalize;
end.

316
UnitTests/DaintyTests.pas Normal file
View File

@ -0,0 +1,316 @@
unit DaintyTests;
interface
uses
Data.DB,
Datasnap.DBClient,
MidasLib,
TestFramework,
Dainty;
type
TDaintyTest = class(TTestCase)
private
FDataSet: TClientDataSet;
protected
procedure SetUp; override;
procedure TearDown; override;
procedure FillTestData(ARowCount: Integer);
property DataSet: TClientDataSet read FDataSet;
published
procedure SimpleTypes;
procedure GetFirst;
procedure GetFirstMultipleRows;
procedure GetFirstNoData;
procedure GetFirstOrDefault;
procedure GetSingle;
procedure GetSingleMultipleRows;
procedure GetSingleNoData;
procedure GetSingleOrDefaultMultipleRows;
procedure GetSingleOrDefaultNoData;
procedure FieldNameAttribute;
procedure FieldNameAttributeProperty;
end;
implementation
uses
System.SysUtils;
{ TDaintyTest }
procedure TDaintyTest.SetUp;
begin
inherited SetUp;
FDataSet := TClientDataSet.Create(nil);
end;
procedure TDaintyTest.TearDown;
begin
FreeAndNil(FDataSet);
inherited TearDown;
end;
type
TSimpleTypesRecord = class
StringField: string;
IntegerField: Integer;
DateTimeField: TDateTime;
BooleanField: Boolean;
FloatField: Double;
end;
procedure TDaintyTest.SimpleTypes;
procedure AddRow(const AStringValue: string; AIntegerValue: Integer; ADateTimeValue: TDateTime; ABooleanValue: Boolean; AFloatValue: Double);
begin
DataSet.Append;
DataSet.FieldByName('STRINGFIELD').AsString := AStringValue;
DataSet.FieldByName('INTEGERFIELD').AsInteger := AIntegerValue;
DataSet.FieldByName('DATETIMEFIELD').AsDateTime := ADateTimeValue;
DataSet.FieldByName('BOOLEANFIELD').AsBoolean := ABooleanValue;
DataSet.FieldByName('FLOATFIELD').AsFloat := AFloatValue;
DataSet.Post;
end;
var
row: TSimpleTypesRecord;
rowIndex: Integer;
begin
DataSet.FieldDefs.Add('STRINGFIELD', ftString, 50);
DataSet.FieldDefs.Add('INTEGERFIELD', ftInteger);
DataSet.FieldDefs.Add('DATETIMEFIELD', ftDate);
DataSet.FieldDefs.Add('BOOLEANFIELD', ftBoolean);
DataSet.FieldDefs.Add('FLOATFIELD', ftFloat);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
AddRow('Hello', 42, EncodeDate(2020, 9, 7), True, 3.1415);
AddRow('world!', 69, EncodeDate(2006, 1, 1), False, 1.618);
DataSet.First;
rowIndex := 0;
for row in DataSet.Rows<TSimpleTypesRecord> do
begin
case rowIndex of
0:
begin
CheckEquals('Hello', row.StringField, 'StringField');
CheckEquals(42, row.IntegerField, 'IntegerField');
CheckEquals(EncodeDate(2020, 9, 7), row.DateTimeField, 0.9, 'DateTimeField');
CheckEquals(True, row.BooleanField, 'BooleanField');
CheckEquals(3.1415, row.FloatField, 0.0001, 'FloatField');
end;
1:
begin
CheckEquals('world!', row.StringField);
CheckEquals(69, row.IntegerField, 'IntegerField');
CheckEquals(EncodeDate(2006, 1, 1), row.DateTimeField, 0.9, 'DateTimeField');
CheckEquals(False, row.BooleanField, 'BooleanField');
CheckEquals(1.618, row.FloatField, 0.0001, 'FloatField');
end;
end;
Inc(rowIndex);
end;
end;
type
TTestRow = class
RowNumber: Integer;
end;
procedure TDaintyTest.FillTestData(ARowCount: Integer);
var
rowNumber: Integer;
begin
DataSet.FieldDefs.Add('ROWNUMBER', ftInteger);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
for rowNumber := 1 to ARowCount do
begin
DataSet.Append;
DataSet.FieldByName('ROWNUMBER').AsInteger := rowNumber;
DataSet.Post;
end;
DataSet.First;
end;
procedure TDaintyTest.GetFirst;
var
row: TTestRow;
begin
FillTestData(1);
row := DataSet.GetFirst<TTestRow>;
CheckEquals(1, row.RowNumber);
end;
procedure TDaintyTest.GetFirstMultipleRows;
var
row: TTestRow;
begin
FillTestData(2);
row := DataSet.GetFirst<TTestRow>;
CheckEquals(1, row.RowNumber);
end;
procedure TDaintyTest.GetFirstNoData;
begin
FillTestData(0);
ExpectedException := EDatabaseError;
DataSet.GetFirst<TTestRow>;
end;
procedure TDaintyTest.GetFirstOrDefault;
var
row: TTestRow;
begin
FillTestData(0);
row := DataSet.GetFirstOrDefault<TTestRow>(nil);
CheckNull(row);
end;
procedure TDaintyTest.GetSingle;
var
row: TTestRow;
begin
FillTestData(1);
row := DataSet.GetSingle<TTestRow>;
CheckEquals(1, row.RowNumber);
end;
procedure TDaintyTest.GetSingleMultipleRows;
begin
ExpectedException := EDatabaseError;
FillTestData(2);
DataSet.GetSingle<TTestRow>;
end;
procedure TDaintyTest.GetSingleNoData;
begin
ExpectedException := EDatabaseError;
FillTestData(0);
DataSet.GetSingle<TTestRow>;
end;
procedure TDaintyTest.GetSingleOrDefaultMultipleRows;
var
row: TTestRow;
begin
FillTestData(2);
row := DataSet.GetSingleOrDefault<TTestRow>(nil);
CheckNull(row);
end;
procedure TDaintyTest.GetSingleOrDefaultNoData;
var
row: TTestRow;
begin
FillTestData(0);
row := DataSet.GetSingleOrDefault<TTestRow>(nil);
CheckNull(row);
end;
type
TAttributeTestRow = class
[FieldName('STRING_FIELD')]
StringField: string;
end;
procedure TDaintyTest.FieldNameAttribute;
var
row: TAttributeTestRow;
begin
DataSet.FieldDefs.Add('STRING_FIELD', ftString, 50);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
DataSet.Append;
DataSet.FieldByName('STRING_FIELD').AsString := 'Hello world!';
DataSet.Post;
row := DataSet.GetFirst<TAttributeTestRow>;
CheckEquals('Hello world!', row.StringField);
end;
type
TPropertyAttributeTestRow = class
private
FStringField: string;
public
[FieldName('STRING_FIELD')]
property StringField: string read FStringField write FStringField;
end;
procedure TDaintyTest.FieldNameAttributeProperty;
var
row: TPropertyAttributeTestRow;
begin
DataSet.FieldDefs.Add('STRING_FIELD', ftString, 50);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
DataSet.Append;
DataSet.FieldByName('STRING_FIELD').AsString := 'Hello world!';
DataSet.Post;
row := DataSet.GetFirst<TPropertyAttributeTestRow>;
CheckEquals('Hello world!', row.StringField);
end;
initialization
RegisterTest(TDaintyTest.Suite);
end.

View File

@ -0,0 +1,14 @@
program DaintyUnitTests;
{$R *.res}
uses
GUITestRunner,
Dainty in '..\Source\Dainty.pas',
DaintyTests in 'DaintyTests.pas',
Dainty.ValueSetter.Default in '..\Source\Dainty.ValueSetter.Default.pas',
DaintyValueSetterTests in 'DaintyValueSetterTests.pas';
begin
RunRegisteredTests;
end.

View File

@ -0,0 +1,142 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{79A2C681-52DB-432C-B68F-67E68341B6C3}</ProjectGuid>
<ProjectVersion>13.4</ProjectVersion>
<FrameworkType>None</FrameworkType>
<MainSource>DaintyUnitTests.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Console</AppType>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
<Base_Win64>true</Base_Win64>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
<Cfg_1_Win32>true</Cfg_1_Win32>
<CfgParent>Cfg_1</CfgParent>
<Cfg_1>true</Cfg_1>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace)</DCC_Namespace>
<DCC_UsePackage>DBXInterBaseDriver;DataSnapServer;DataSnapCommon;DbxCommonDriver;dbxcds;CustomIPTransport;dsnap;IndyCore;FmxTeeUI;IPIndyImpl;bindcompfmx;dbrtl;bindcomp;inetdb;xmlrtl;ibxpress;soaprtl;FMXTee;bindengine;DBXInformixDriver;DBXFirebirdDriver;inet;DBXSybaseASADriver;dbexpress;fmx;IndySystem;DataSnapClient;DataSnapProviderClient;DBXOracleDriver;inetdbxpress;rtl;DbxClientDriver;IndyProtocols;DBXMySQLDriver;DataSnapIndy10ServerTransport;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_DcuOutput>.\$(Platform)\$(Config)</DCC_DcuOutput>
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<DCC_UsePackage>dxTileControlRS16;cxLibraryRS16;dxPsPrVwAdvRS16;dxRibbonCustomizationFormRS16;dxPSCoreRS16;dxRichEditInplaceRS16;dxPScxTLLnkRS16;dxPSdxSpreadSheetLnkRS16;cxPageControlRS16;dxPScxGridLnkRS16;vclimg;dxComnRS16;vcldb;dxOfficeCoreRS16;dxBarExtDBItemsRS16;dxSpreadSheetInplaceRichEditRS16;dxSpreadSheetCoreRS16;cxDataRS16;dxDockingRS16;dxPSDBTeeChartRS16;dxSpreadSheetReportDesignerRS16;dxBarExtItemsRS16;dxPSLnksRS16;dxPSdxPDFViewerLnkRS16;dxPSTeeChartRS16;dxPSdxLCLnkRS16;cxVerticalGridRS16;dxorgcRS16;dxWizardControlRS16;dxPScxExtCommonRS16;dxNavBarRS16;dxSpreadSheetCoreDialogsRS16;dxSpreadSheetCoreConditionalFormattingDialogsRS16;cxSchedulerTreeBrowserRS16;DBXOdbcDriver;dxdbtrRS16;dxRichEditCoreRS16;dxPSdxDBTVLnkRS16;vclactnband;dxPScxSchedulerLnkRS16;dxServerModeRS16;bindcompvcl;vclie;dsnapcon;dxHttpIndyRequestRS16;vclx;dxFlowChartRS16;dxGDIPlusRS16;dxdborRS16;dxPDFViewerRS16;dxEMFRS16;dxLayoutControlRS16;dxSpreadSheetRS16;dxPScxPivotGridLnkRS16;dxCoreRS16;dxPScxVGridLnkRS16;cxExportRS16;dxPSdxMapControlLnkRS16;dxBarRS16;TeeDB;dxDBXServerModeRS16;cxTreeListdxBarPopupMenuRS16;dxPSdxGaugeControlLnkRS16;dxRibbonRS16;DBXSybaseASEDriver;dxSpreadSheetConditionalFormattingDialogsRS16;cxTreeListRS16;vcldsnap;dxGaugeControlRS16;DBXDb2Driver;vcl;DBXMSSQLDriver;cxSchedulerGridRS16;dxRichEditControlCoreRS16;webdsnap;dxtrmdRS16;adortl;dxRichEditControlRS16;dxRichEditDocumentModelRS16;dxPSdxDBOCLnkRS16;Tee;cxPivotGridChartRS16;dxmdsRS16;dxSpellCheckerRS16;dxMapControlRS16;cxGridRS16;dxPScxCommonRS16;dxPSPrVwRibbonRS16;cxEditorsRS16;TeeUI;cxPivotGridRS16;cxSchedulerRibbonStyleEventEditorRS16;cxSchedulerRS16;vcltouch;websnap;dxFlowChartAdvancedCustomizeFormRS16;VclSmp;dxTabbedMDIRS16;dxPSRichEditControlLnkRS16;dxPSdxOCLnkRS16;dxPSdxFCLnkRS16;dxThemeRS16;dxPScxPCProdRS16;dxBarDBNavRS16;$(DCC_UsePackage)</DCC_UsePackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<DCC_UsePackage>dxTileControlRS16;JvGlobus;JvMM;cxLibraryRS16;JvManagedThreads;JvCrypt;rbDIDE1516;dxPsPrVwAdvRS16;dxRibbonCustomizationFormRS16;rbTCUI1516;dxPSCoreRS16;dxRichEditInplaceRS16;dxPScxTLLnkRS16;dxPSdxSpreadSheetLnkRS16;JvNet;cxPageControlRS16;dxPScxGridLnkRS16;JvDotNetCtrls;vclimg;rbDAD1516;dxComnRS16;JvXPCtrls;rbRCL1516;vcldb;dxOfficeCoreRS16;dxBarExtDBItemsRS16;rbIBE1516;dxSpreadSheetInplaceRichEditRS16;dclRBIBE1516;dclRBADO1516;dxSpreadSheetCoreRS16;CloudService;rbRAP1516;cxDataRS16;dxDockingRS16;dxPSDBTeeChartRS16;JvDB;JvRuntimeDesign;dxSpreadSheetReportDesignerRS16;X2CLMB;dxBarExtItemsRS16;dxPSLnksRS16;dxPSdxPDFViewerLnkRS16;JclDeveloperTools;dxPSTeeChartRS16;dxPSdxLCLnkRS16;cxVerticalGridRS16;dxorgcRS16;dxWizardControlRS16;dxPScxExtCommonRS16;vcldbx;rbDBDE1516;dxNavBarRS16;dclRBE1516;dxSpreadSheetCoreDialogsRS16;rbIDE1516;JvPluginSystem;dxSpreadSheetCoreConditionalFormattingDialogsRS16;cxSchedulerTreeBrowserRS16;DBXOdbcDriver;JvCmp;dxdbtrRS16;madDisAsm_;dxRichEditCoreRS16;JvTimeFramework;dxPSdxDBTVLnkRS16;vclactnband;dxPScxSchedulerLnkRS16;dxServerModeRS16;bindcompvcl;Jcl;vclie;madExcept_;dsnapcon;dxHttpIndyRequestRS16;JvPascalInterpreter;vclx;rbDBE1516;dxFlowChartRS16;dxGDIPlusRS16;JvBDE;dxdborRS16;dxPDFViewerRS16;dxEMFRS16;dxLayoutControlRS16;dxSpreadSheetRS16;dxPScxPivotGridLnkRS16;dxCoreRS16;dxPScxVGridLnkRS16;cxExportRS16;dxPSdxMapControlLnkRS16;dxBarRS16;JvDlgs;cxSchedulerWebServiceStorageRS16;TeeDB;dxDBXServerModeRS16;cxTreeListdxBarPopupMenuRS16;dxPSdxGaugeControlLnkRS16;vclib;inetdbbde;dxRibbonRS16;DBXSybaseASEDriver;dxSpreadSheetConditionalFormattingDialogsRS16;rbTC1516;cxTreeListRS16;vcldsnap;X2CLGL;dxGaugeControlRS16;DBXDb2Driver;dclRBBDE1516;rbDB1516;JvCore;vcl;DBXMSSQLDriver;JvAppFrm;cxSchedulerGridRS16;dxRichEditControlCoreRS16;webdsnap;dxtrmdRS16;rbRIDE1516;JvDocking;adortl;JvWizards;dxRichEditControlRS16;rbADO1516;madBasic_;JvHMI;dxRichEditDocumentModelRS16;JvBands;dxPSdxDBOCLnkRS16;Tee;rbUSERDesign1516;JvSystem;svnui;cxPivotGridChartRS16;JvControls;dxmdsRS16;dxSpellCheckerRS16;dxMapControlRS16;cxGridRS16;dxPScxCommonRS16;dxPSPrVwRibbonRS16;cxEditorsRS16;TeeUI;cxPivotGridRS16;rbUSER1516;JvJans;JvPrintPreview;JvPageComps;dclRBDBE1516;JvStdCtrls;JvCustom;cxSchedulerRibbonStyleEventEditorRS16;rbBDE1516;cxSchedulerRS16;vcltouch;websnap;dxFlowChartAdvancedCustomizeFormRS16;VclSmp;dxTabbedMDIRS16;dxPSRichEditControlLnkRS16;DataSnapConnectors;dxPSdxOCLnkRS16;dxPSdxFCLnkRS16;dxThemeRS16;JclVcl;dxPScxPCProdRS16;rbCIDE1516;svn;bdertl;VirtualTreesR;dxAuthorizationAgentsRS16;dxBarDBNavRS16;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
<VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
<DCC_Optimize>false</DCC_Optimize>
<DCC_GenerateStackFrames>true</DCC_GenerateStackFrames>
<DCC_DebugInfoInExe>true</DCC_DebugInfoInExe>
<DCC_RemoteDebug>true</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
<DCC_DebugDCUs>true</DCC_DebugDCUs>
<Manifest_File>None</Manifest_File>
<VerInfo_Locale>1033</VerInfo_Locale>
<DCC_RemoteDebug>false</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>false</DCC_DebugInformation>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="..\Source\Dainty.pas"/>
<DCCReference Include="DaintyTests.pas"/>
<DCCReference Include="..\Source\Dainty.ValueSetter.Default.pas"/>
<DCCReference Include="DaintyValueSetterTests.pas"/>
<BuildConfiguration Include="Release">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Delphi.Personality>
<VersionInfo>
<VersionInfo Name="IncludeVerInfo">False</VersionInfo>
<VersionInfo Name="AutoIncBuild">False</VersionInfo>
<VersionInfo Name="MajorVer">1</VersionInfo>
<VersionInfo Name="MinorVer">0</VersionInfo>
<VersionInfo Name="Release">0</VersionInfo>
<VersionInfo Name="Build">0</VersionInfo>
<VersionInfo Name="Debug">False</VersionInfo>
<VersionInfo Name="PreRelease">False</VersionInfo>
<VersionInfo Name="Special">False</VersionInfo>
<VersionInfo Name="Private">False</VersionInfo>
<VersionInfo Name="DLL">False</VersionInfo>
<VersionInfo Name="Locale">1043</VersionInfo>
<VersionInfo Name="CodePage">1252</VersionInfo>
</VersionInfo>
<VersionInfoKeys>
<VersionInfoKeys Name="CompanyName"/>
<VersionInfoKeys Name="FileDescription"/>
<VersionInfoKeys Name="FileVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="InternalName"/>
<VersionInfoKeys Name="LegalCopyright"/>
<VersionInfoKeys Name="LegalTrademarks"/>
<VersionInfoKeys Name="OriginalFilename"/>
<VersionInfoKeys Name="ProductName"/>
<VersionInfoKeys Name="ProductVersion">1.0.0.0</VersionInfoKeys>
<VersionInfoKeys Name="Comments"/>
</VersionInfoKeys>
<Source>
<Source Name="MainSource">DaintyUnitTests.dpr</Source>
</Source>
</Delphi.Personality>
<Deployment/>
<Platforms>
<Platform value="Win64">False</Platform>
<Platform value="Win32">True</Platform>
</Platforms>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')" Project="$(BDS)\Bin\CodeGear.Delphi.Targets"/>
<Import Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')" Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj"/>
</Project>

View File

@ -0,0 +1,171 @@
unit DaintyValueSetterTests;
interface
uses
Data.DB,
DataSnap.DBClient,
MidasLib,
TestFramework,
Dainty;
type
TDaintyValueSetterTest = class(TTestCase)
private
FDataSet: TClientDataSet;
protected
procedure SetUp; override;
procedure TearDown; override;
property DataSet: TClientDataSet read FDataSet;
published
procedure CustomRecordMapping;
procedure GenericCustomRecordMapping;
end;
implementation
uses
System.Rtti,
System.SysUtils,
System.TypInfo,
System.Variants;
{ TDaintyValueSetterTest }
procedure TDaintyValueSetterTest.SetUp;
begin
inherited SetUp;
FDataSet := TClientDataSet.Create(nil);
end;
procedure TDaintyValueSetterTest.TearDown;
begin
FreeAndNil(FDataSet);
inherited TearDown;
end;
type
TCustomRecord = record
Value: string;
end;
TCustomRecordRow = class
StringField: TCustomRecord;
end;
{ Mimics our proprietary implementation of generic nullables, which is based on:
https://community.embarcadero.com/blogs/entry/a-andquotnullableandquot-post-38869 }
TGenericCustomRecord<T> = record
Value: T;
end;
TGenericCustomRecordRow = class
StringField: TGenericCustomRecord<string>;
end;
TCustomRecordValueSetterFactory = class(TDaintyAbstractValueSetterFactory)
public
class function Construct(AMember: TDaintyRttiMember): TDaintyValueSetter; override;
end;
{ TCustomRecordValueSetterFactory }
class function TCustomRecordValueSetterFactory.Construct(AMember: TDaintyRttiMember): TDaintyValueSetter;
begin
if AMember.RttiType.TypeKind <> tkRecord then
Exit(nil);
if AMember.RttiType.Handle = TypeInfo(TCustomRecord) then
begin
Result :=
procedure(AInstance: TObject; AField: TField)
var
customRecord: TCustomRecord;
begin
customRecord.Value := AField.AsString;
AMember.SetValue(AInstance, TValue.From(customRecord));
end;
{ Unfortunately I have not found a way to make this generic, if you do let me know!
The workaround is to handle all types you want to support explicitly, which is
good enough for our use case with nullable implementations. }
end else if AMember.RttiType.Handle = TypeInfo(TGenericCustomRecord<string>) then
begin
Result :=
procedure(AInstance: TObject; AField: TField)
var
value: TGenericCustomRecord<string>;
begin
value.Value := AField.AsString;
AMember.SetValue(AInstance, TValue.From(value));
end;
end;
end;
procedure TDaintyValueSetterTest.CustomRecordMapping;
var
row: TCustomRecordRow;
begin
DataSet.FieldDefs.Add('STRINGFIELD', ftString, 50);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
DataSet.Append;
DataSet.FieldByName('STRINGFIELD').AsString := 'Hello world!';
DataSet.Post;
TDaintyRttiMapperFactory.RegisterValueSetterFactory(TCustomRecordValueSetterFactory, 1);
try
DataSet.First;
row := DataSet.GetFirst<TCustomRecordRow>;
CheckEquals('Hello world!', row.StringField.Value);
finally
TDaintyRttiMapperFactory.UnregisterValueSetterFactory(TCustomRecordValueSetterFactory);
end;
end;
procedure TDaintyValueSetterTest.GenericCustomRecordMapping;
var
row: TGenericCustomRecordRow;
begin
DataSet.FieldDefs.Add('STRINGFIELD', ftString, 50);
DataSet.CreateDataSet;
DataSet.LogChanges := False;
DataSet.Append;
DataSet.FieldByName('STRINGFIELD').AsString := 'Hello world!';
DataSet.Post;
TDaintyRttiMapperFactory.RegisterValueSetterFactory(TCustomRecordValueSetterFactory, 1);
try
DataSet.First;
row := DataSet.GetFirst<TGenericCustomRecordRow>;
CheckEquals('Hello world!', row.StringField.Value);
finally
TDaintyRttiMapperFactory.UnregisterValueSetterFactory(TCustomRecordValueSetterFactory);
end;
end;
initialization
RegisterTest(TDaintyValueSetterTest.Suite);
end.