diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 397304d..53f04e4 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -39,6 +39,7 @@ jobs: --logger "trx" --results-directory "${{ github.workspace }}/TestResults" --collect:"XPlat Code Coverage" + /p:Exclude="[idou.*.Tests*]*" - name: Combine Coverage Reports if: always() diff --git a/.gitignore b/.gitignore index 4b1dfcb..4f2872d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,343 @@ -bin/ -obj/ +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper *.DotSettings.user +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs +Resource.designer.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake # +/tools/** +/build/tools/** +/build/temp/** + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/*/.idea/projectSettingsUpdater.xml +.idea/*/.idea/encodings.xml +.idea/*/.idea/vcs.xml +.idea/*/.idea/indexLayout.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# fastlane +fastlane/report.xml + +# inspectcode +inspectcodereport.xml +inspectcode + +# BenchmarkDotNet +/BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig + + +.idea/.idea.idou/.idea/misc.xml +.idea/.idea.idou/.idea/copilot.* diff --git a/.idea/.idea.idou/.idea/.gitignore b/.idea/.idea.idou/.idea/.gitignore new file mode 100644 index 0000000..e247bb2 --- /dev/null +++ b/.idea/.idea.idou/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +/.idea.idou.iml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/docs/flow.puml b/docs/flow.puml new file mode 100644 index 0000000..feaa067 --- /dev/null +++ b/docs/flow.puml @@ -0,0 +1,82 @@ +@startuml + +title Sync Job Execution Flow + +start + +:SyncEngine.RunAsync(job); + +:JobStateStore.SetStatus(Running); + +:Load Checkpoint; +note right +ICheckpointStore.LoadAsync(jobId) +end note + +if (Checkpoint exists?) then (no) + :Bootstrap mode; + repeat + :Read snapshot page; + note right + IDataSource.ReadSnapshotAsync + end note + + :Map & Transform; + note right + ChangePipeline + - IMappingPlan + - ITransformer[] + end note + + :Apply upserts; + note right + IDataTarget.UpsertAsync + end note + + :Update progress; + note right + IJobStateStore.UpdateProgress + end note + repeat while (More pages?) +else (yes) + :Incremental mode; + repeat + :Read changes; + note right + IDataSource.ReadChangesAsync + end note + + :Map & Transform; + note right + ChangePipeline + end note + + if (Conflict resolution enabled?) then (yes) + :Resolve conflicts; + note right + IConflictResolver.Resolve + (may call IDataTarget.TryGetAsync) + end note + endif + + :Apply changes; + note right + IDataTarget.ApplyChangesAsync + end note + + :Save checkpoint; + note right + ICheckpointStore.SaveAsync + end note + + :Update progress; + note right + IJobStateStore.UpdateProgress + end note + repeat while (Continue & not cancelled) +endif + +:JobStateStore.SetStatus(Stopped); + +stop +@enduml diff --git a/idou.Core.Tests/Domain/ChangeEventTests.cs b/idou.Core.Tests/Domain/ChangeEventTests.cs new file mode 100644 index 0000000..203b703 --- /dev/null +++ b/idou.Core.Tests/Domain/ChangeEventTests.cs @@ -0,0 +1,39 @@ +using idou.Core.Domain; +using idou.Core.Domain.Enums; + +namespace idou.Core.Tests.Domain; + +public class ChangeEventTests +{ + [Fact] + public void ChangeEvent_Creation_Works() + { + const ChangeOperation upsertMode = ChangeOperation.Upsert; + var offset = DateTimeOffset.UtcNow; + var payload = new EntityRecord + { + Type = new EntityType("TestType"), + Key = new EntityKey("TestKey"), + Attributes = new Dictionary() + }; + + var changeEvent = new ChangeEvent(offset) + { + Operation = upsertMode, + Key = new EntityKey("TestKey"), + Type = new EntityType("TestType"), + Version = "1.0.0", + Payload = payload, + Metadata = new Dictionary() + }; + + Assert.NotNull(changeEvent); + Assert.Equal(upsertMode, changeEvent.Operation); + Assert.Equal("TestKey", changeEvent.Key.Value); + Assert.Equal("TestType", changeEvent.Type.ToString()); + Assert.Equal(offset, changeEvent.Timestamp); + Assert.Equal("1.0.0", changeEvent.Version); + Assert.Equal(payload, changeEvent.Payload); + Assert.NotNull(changeEvent.Metadata); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Domain/ChangeFilterTests.cs b/idou.Core.Tests/Domain/ChangeFilterTests.cs new file mode 100644 index 0000000..13ec6c9 --- /dev/null +++ b/idou.Core.Tests/Domain/ChangeFilterTests.cs @@ -0,0 +1,22 @@ +using idou.Core.Domain; + +namespace idou.Core.Tests.Domain; + +public class ChangeFilterTests +{ + [Fact] + public void ChangeFilter_Constructor_SetsProperties() + { + var entityTypes = new List + { + new("TypeA"), + new("TypeB") + }; + var since = DateTimeOffset.UtcNow.AddDays(-1); + + var changeFilter = new ChangeFilter(entityTypes, since); + + Assert.Equal(entityTypes, changeFilter.EntityTypes); + Assert.Equal(since, changeFilter.Since); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Domain/EntityKeyTests.cs b/idou.Core.Tests/Domain/EntityKeyTests.cs new file mode 100644 index 0000000..de36be0 --- /dev/null +++ b/idou.Core.Tests/Domain/EntityKeyTests.cs @@ -0,0 +1,20 @@ +using idou.Core.Domain; + +namespace idou.Core.Tests.Domain; + +public class EntityKeyTests +{ + [Fact] + public void EntityKey_Creation_Works() + { + const string keyName = "TestKey"; + + var entityKey = new EntityKey(keyName); + + Assert.Equal(keyName, entityKey.Value); + Assert.Equal(keyName, entityKey.ToString()); + Assert.Equal(keyName.GetHashCode(), entityKey.GetHashCode()); + Assert.True(entityKey.Equals(new EntityKey(keyName))); + Assert.False(entityKey.Equals(new EntityKey("DifferentKey"))); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Domain/EntityRecordTests.cs b/idou.Core.Tests/Domain/EntityRecordTests.cs new file mode 100644 index 0000000..98fbe5a --- /dev/null +++ b/idou.Core.Tests/Domain/EntityRecordTests.cs @@ -0,0 +1,23 @@ +using idou.Core.Domain; + +namespace idou.Core.Tests.Domain; + +public class EntityRecordTests +{ + [Fact] + public void EntityRecord_Creation_Works() + { + var entityRecord = new EntityRecord + { + Key = new EntityKey("TestKey"), + Type = new EntityType("TestType"), + Attributes = new Dictionary() + }; + + Assert.NotNull(entityRecord); + + Assert.Equal("TestKey", entityRecord.Key.Value); + Assert.Equal("TestType", entityRecord.Type.ToString()); + Assert.NotNull(entityRecord.Attributes); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Domain/EntityTypeTests.cs b/idou.Core.Tests/Domain/EntityTypeTests.cs new file mode 100644 index 0000000..07f53b1 --- /dev/null +++ b/idou.Core.Tests/Domain/EntityTypeTests.cs @@ -0,0 +1,19 @@ +using idou.Core.Domain; + +namespace idou.Core.Tests.Domain; + +public class EntityTypeTests +{ + [Fact] + public void EntityType_Creation_Works() + { + const string typeName = "TestType"; + + var entityType = new EntityType(typeName); + + Assert.Equal(typeName, entityType.ToString()); + Assert.Equal(typeName.GetHashCode(), entityType.GetHashCode()); + Assert.True(entityType.Equals(new EntityType(typeName))); + Assert.False(entityType.Equals(new EntityType("DifferentType"))); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Engine/ChangePipelineTests.cs b/idou.Core.Tests/Engine/ChangePipelineTests.cs index 2315741..433b144 100644 --- a/idou.Core.Tests/Engine/ChangePipelineTests.cs +++ b/idou.Core.Tests/Engine/ChangePipelineTests.cs @@ -180,6 +180,8 @@ public void MapAndTransform_IfTransformerTurnsEventIntoNoOp_ItIsFilteredOut() private sealed class RecordingMappingPlan : IMappingPlan { + public bool IsValid => true; + private readonly Func _mapEntityType; private readonly Func _mapKey; private readonly Func _mapRecord; diff --git a/idou.Core.Tests/Mapping/DefaultMappingPlanTests.cs b/idou.Core.Tests/Mapping/DefaultMappingPlanTests.cs new file mode 100644 index 0000000..4f01c44 --- /dev/null +++ b/idou.Core.Tests/Mapping/DefaultMappingPlanTests.cs @@ -0,0 +1,22 @@ +using idou.Core.Mapping; + +namespace idou.Core.Tests.Mapping; + +public class DefaultMappingPlanTests +{ + [Fact] + public void DefaultMappingPlan_Creation_Works() + { + var mappingPlan = new DefaultMappingPlan(); + + Assert.NotNull(mappingPlan); + } + + [Fact] + public void DefaultMappingPlan_IsValidByDefault() + { + var mappingPlan = new DefaultMappingPlan(); + + Assert.True(mappingPlan.IsValid); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Mapping/JsonMappingPlanTests.cs b/idou.Core.Tests/Mapping/JsonMappingPlanTests.cs new file mode 100644 index 0000000..070fc7f --- /dev/null +++ b/idou.Core.Tests/Mapping/JsonMappingPlanTests.cs @@ -0,0 +1,93 @@ +using idou.Core.Domain; +using idou.Core.Mapping; + +namespace idou.Core.Tests.Mapping; + +public class JsonMappingPlanTests +{ + [Fact] + public void MappingPlanIsValid_WhenTheFileExistsAndHasMappings() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + Assert.True(plan.IsValid); + } + + [Fact] + public void Throws_WhenTheFileDoesNotExist() + { + Assert.Throws(() => new JsonMappingPlan("Mapping/assets/does-not-exist.json")); + } + + [Fact] + public void MappingPlanIsInvalid_WhenTheMappingsAreEmpty() + { + var plan = new JsonMappingPlan("Mapping/assets/empty.json"); + Assert.False(plan.IsValid); + } + + [Fact] + public void MapsTheEntityType_WhenTheMappingExists() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + + var sourceType = new EntityType("users"); + var mapped = plan.MapEntityType(sourceType); + + Assert.True(plan.IsValid); + Assert.Equal("accounts", mapped.Name); + } + + [Fact] + public void MapsTheEntityType_CaseInsensitive_WhenTheMappingExists() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + var sourceType = new EntityType("UsErS"); + var mapped = plan.MapEntityType(sourceType); + Assert.True(plan.IsValid); + Assert.Equal("accounts", mapped.Name); + } + + [Fact] + public void DoesNotMapTheEntityType_WhenNoMappingExists() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + + var sourceType = new EntityType("unknown"); + var mapped = plan.MapEntityType(sourceType); + + Assert.Equal(sourceType.Name, mapped.Name); + } + + [Fact] + public void MapsTheEntityKey_WhenTheMappingExists() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + var sourceType = new EntityType("users"); + var sourceKey = new EntityKey("u1"); + + var mappedKey = plan.MapKey(sourceType, sourceKey); + + Assert.Equal(sourceKey.Value, mappedKey.Value); + } + + [Fact] + public void MapsTheEntityRecord_WhenTheMappingExists() + { + var plan = new JsonMappingPlan("Mapping/assets/valid.json"); + var sourceRecord = new EntityRecord + { + Type = new EntityType("users"), + Key = new EntityKey("u1"), + Attributes = new Dictionary + { + { "name", "Alice" } + } + }; + + var mappedRecord = plan.MapRecord(sourceRecord); + + Assert.Equal("accounts", mappedRecord.Type.Name); + Assert.Equal(sourceRecord.Key.Value, mappedRecord.Key.Value); + Assert.Equal(sourceRecord.Attributes["name"], mappedRecord.Attributes["name"]); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Mapping/JsonMappingTests.cs b/idou.Core.Tests/Mapping/JsonMappingTests.cs new file mode 100644 index 0000000..a4dd648 --- /dev/null +++ b/idou.Core.Tests/Mapping/JsonMappingTests.cs @@ -0,0 +1,22 @@ +using idou.Core.Mapping; + +namespace idou.Core.Tests.Mapping; + +public class JsonMappingTests +{ + [Fact] + public void JsonMapping_Creation_Works() + { + const string source = "sourceField"; + const string target = "targetField"; + + var mapping = new JsonMapping + { + Source = source, + Target = target + }; + + Assert.Equal(source, mapping.Source); + Assert.Equal(target, mapping.Target); + } +} \ No newline at end of file diff --git a/idou.Core.Tests/Mapping/assets/empty.json b/idou.Core.Tests/Mapping/assets/empty.json new file mode 100644 index 0000000..32960f8 --- /dev/null +++ b/idou.Core.Tests/Mapping/assets/empty.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/idou.Core.Tests/Mapping/assets/valid.json b/idou.Core.Tests/Mapping/assets/valid.json new file mode 100644 index 0000000..8b46f84 --- /dev/null +++ b/idou.Core.Tests/Mapping/assets/valid.json @@ -0,0 +1,6 @@ +[ + { + "source": "users", + "target": "accounts" + } +] \ No newline at end of file diff --git a/idou.Core.Tests/idou.Core.Tests.csproj b/idou.Core.Tests/idou.Core.Tests.csproj index 5ec0c9a..a9fbde5 100644 --- a/idou.Core.Tests/idou.Core.Tests.csproj +++ b/idou.Core.Tests/idou.Core.Tests.csproj @@ -10,6 +10,7 @@ + @@ -22,4 +23,13 @@ + + + Always + + + Always + + + \ No newline at end of file diff --git a/idou.Core/Capabilities/CapabilitySet.cs b/idou.Core/Capabilities/CapabilitySet.cs deleted file mode 100644 index 27dd041..0000000 --- a/idou.Core/Capabilities/CapabilitySet.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace idou.Core.Capabilities; - -public class CapabilitySet -{ - public bool SupportsChangeFeed { get; init; } - public bool SupportsSnapshotRead { get; init; } - public bool SupportsBulkWrite { get; init; } - public bool SupportsTransactions { get; init; } - public bool SupportsConditionalWrite { get; init; } - -} \ No newline at end of file diff --git a/idou.Core/Domain/ChangeFilter.cs b/idou.Core/Domain/ChangeFilter.cs index 43a37f0..007cde4 100644 --- a/idou.Core/Domain/ChangeFilter.cs +++ b/idou.Core/Domain/ChangeFilter.cs @@ -1,7 +1,7 @@ namespace idou.Core.Domain; -public class ChangeFilter +public class ChangeFilter(IReadOnlyCollection entityTypes, DateTimeOffset? since) { - public IReadOnlyCollection EntityTypes { get; } - public DateTimeOffset? Since { get; } + public IReadOnlyCollection EntityTypes { get; } = entityTypes; + public DateTimeOffset? Since { get; } = since; } \ No newline at end of file diff --git a/idou.Core/Engine/RetryPolicy.cs b/idou.Core/Engine/RetryPolicy.cs deleted file mode 100644 index 707deb8..0000000 --- a/idou.Core/Engine/RetryPolicy.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace idou.Core.Engine; - -public class RetryPolicy -{ - -} \ No newline at end of file diff --git a/idou.Core/Engine/SyncEngine.cs b/idou.Core/Engine/SyncEngine.cs deleted file mode 100644 index f9996b0..0000000 --- a/idou.Core/Engine/SyncEngine.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace idou.Core.Engine; - -public class SyncEngine -{ - -} \ No newline at end of file diff --git a/idou.Core/Engine/SyncJob.cs b/idou.Core/Engine/SyncJob.cs deleted file mode 100644 index 0cd331a..0000000 --- a/idou.Core/Engine/SyncJob.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace idou.Core.Engine; - -public class SyncJob -{ - -} \ No newline at end of file diff --git a/idou.Core/Mapping/DefaultMappingPlan.cs b/idou.Core/Mapping/DefaultMappingPlan.cs index ad45796..361c408 100644 --- a/idou.Core/Mapping/DefaultMappingPlan.cs +++ b/idou.Core/Mapping/DefaultMappingPlan.cs @@ -4,6 +4,8 @@ namespace idou.Core.Mapping; public class DefaultMappingPlan : IMappingPlan { + public bool IsValid => true; + public EntityType MapEntityType(EntityType sourceType) { return sourceType; diff --git a/idou.Core/Mapping/IMappingPlan.cs b/idou.Core/Mapping/IMappingPlan.cs index d52ac97..53f425e 100644 --- a/idou.Core/Mapping/IMappingPlan.cs +++ b/idou.Core/Mapping/IMappingPlan.cs @@ -4,6 +4,7 @@ namespace idou.Core.Mapping; public interface IMappingPlan { + bool IsValid { get; } EntityType MapEntityType(EntityType sourceType); EntityKey MapKey(EntityType sourceType, EntityKey sourceKey); EntityRecord MapRecord(EntityRecord sourceRecord); diff --git a/idou.Core/Mapping/JsonMapping.cs b/idou.Core/Mapping/JsonMapping.cs new file mode 100644 index 0000000..40e7e23 --- /dev/null +++ b/idou.Core/Mapping/JsonMapping.cs @@ -0,0 +1,7 @@ +namespace idou.Core.Mapping; + +public class JsonMapping +{ + public required string Source { get; init; } = null!; + public required string Target { get; init; } = null!; +} \ No newline at end of file diff --git a/idou.Core/Mapping/JsonMappingPlan.cs b/idou.Core/Mapping/JsonMappingPlan.cs new file mode 100644 index 0000000..0e77d20 --- /dev/null +++ b/idou.Core/Mapping/JsonMappingPlan.cs @@ -0,0 +1,48 @@ +using idou.Core.Domain; +using Newtonsoft.Json; + +namespace idou.Core.Mapping; + +public sealed class JsonMappingPlan : IMappingPlan +{ + private readonly IReadOnlyDictionary _bySource; + + public JsonMappingPlan(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException($"Mapping file not found: {filePath}"); + + var mappings = JsonConvert.DeserializeObject>( + File.ReadAllText(filePath) + ) ?? throw new InvalidOperationException("Invalid mapping file"); + + _bySource = mappings.ToDictionary(m => m.Source, StringComparer.OrdinalIgnoreCase); + } + + public bool IsValid => _bySource.Count > 0; + + public EntityType MapEntityType(EntityType sourceType) + { + return _bySource.TryGetValue(sourceType.Name, out var mapping) + ? new EntityType(mapping.Target) + : sourceType; + } + + public EntityKey MapKey(EntityType sourceType, EntityKey sourceKey) + { + return sourceKey; + } + + public EntityRecord MapRecord(EntityRecord sourceRecord) + { + var targetType = MapEntityType(sourceRecord.Type); + var targetKey = MapKey(sourceRecord.Type, sourceRecord.Key); + + return new EntityRecord + { + Key = targetKey, + Type = targetType, + Attributes = new Dictionary(sourceRecord.Attributes) + }; + } +} \ No newline at end of file diff --git a/idou.Core/Results/ApplyResult.cs b/idou.Core/Results/ApplyResult.cs deleted file mode 100644 index 1ee7d8a..0000000 --- a/idou.Core/Results/ApplyResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace idou.Core.Results; - -public class ApplyResult -{ - public int AppliedCount { get; } - public int FailedCount { get; } - public IReadOnlyList Errors { get; } -} \ No newline at end of file diff --git a/idou.Core/Results/WriteResult.cs b/idou.Core/Results/WriteResult.cs deleted file mode 100644 index 6c05df0..0000000 --- a/idou.Core/Results/WriteResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace idou.Core.Results; - -public class WriteResult -{ - public int WrittenCount { get; } - public int FailedCount { get; } - IReadOnlyList Errors { get; } -} \ No newline at end of file diff --git a/idou.Core/idou.Core.csproj b/idou.Core/idou.Core.csproj index 237d661..3d6217c 100644 --- a/idou.Core/idou.Core.csproj +++ b/idou.Core/idou.Core.csproj @@ -6,4 +6,8 @@ enable + + + +