Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.obsidian/
**/bin/
**/obj/
27 changes: 27 additions & 0 deletions Mapper.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapper", "src/Mapper/Mapper.csproj", "{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapper.Tests", "src/Mapper.Tests/Mapper.Tests.csproj", "{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Release|Any CPU.Build.0 = Release|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Mapper

Библиотека на C# для преобразования `JsonPatchDocument<TSource>` в `JsonPatchDocument<TTarget>`.

## Что реализовано

- мэппинг одноимённых полей по умолчанию;
- автоматическая конвертация значения при различии типов;
- явное переименование целевого поля через `ForMember(...).MapFrom(...)`;
- игнорирование поля через `ForMember(...).Ignore()`;
- вычисляемое преобразование значения из одного исходного поля.

## Структура

- [src/Mapper.sln](/home/me/projects/mapper/src/Mapper.sln) — solution в папке исходников;
- [src/Mapper](/home/me/projects/mapper/src/Mapper) — библиотека;
- [src/Mapper.Tests](/home/me/projects/mapper/src/Mapper.Tests) — тесты `xUnit`.

## Использование

```csharp
public sealed class RequestProfile : MapProfile
{
public RequestProfile()
{
CreateMap<SourceModel, TargetModel>()
.ForMember(target => target.DisplayName, options => options.MapFrom(source => source.Name));
}
}

var mapper = new Mapper(new RequestProfile());
var targetPatch = mapper.Map<SourceModel, TargetModel>(sourcePatch);
```

## Тесты

Из корня репозитория:

```bash
dotnet test
```
1 change: 1 addition & 0 deletions TASK-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Создай проект по описанию из PROJECT.md
1 change: 1 addition & 0 deletions homeworks/hw-0/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@TASK-1
21 changes: 21 additions & 0 deletions homeworks/hw-0/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Что сделал агент?
- создал проект
- наполнил его функциональностью по спеке
- сбилдил (подтянув нужное через mise)
- кажется, запускал тесты

Какие файлы он изменил или собирался изменить?
- полностью создал запускаемый проект

Как вы проверили результат?
- поверил агенту, что проект билдится
- просмотрел исходники глазами

Где агент ошибся или потерял контекст?
- по задумке мэппинг должен настраиваться с помощью кодогенерации, а он повел как-то не так

В каком месте вам захотелось его остановить?
- не захотелось

Какие 3-5 уточнений вы бы добавили в следующий заход?
- нужно как-то направить реализацию в нужное русло. Пока не сформулировал
8 changes: 8 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
20 changes: 20 additions & 0 deletions src/Mapper.Tests/Mapper.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Mapper.Tests</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.24" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Mapper\Mapper.csproj" />
</ItemGroup>
</Project>
180 changes: 180 additions & 0 deletions src/Mapper.Tests/MapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Xunit;

namespace Mapper.Tests;

public sealed class MapperTests
{
[Fact]
public void Map_Maps_Add_Replace_Remove_And_Test_For_Same_Named_Members()
{
var mapper = new global::Mapper.Mapper(new DefaultProfile());
var patch = CreatePatch<SourceModel>(
Operation<SourceModel>("add", "/Name", value: "Alice"),
Operation<SourceModel>("replace", "/Age", value: 42),
Operation<SourceModel>("remove", "/Name"),
Operation<SourceModel>("test", "/Age", value: 42));

var mapped = mapper.Map<SourceModel, TargetModel>(patch);

Assert.Collection(
mapped.Operations,
operation =>
{
Assert.Equal("add", operation.op);
Assert.Equal("/Name", operation.path);
Assert.Equal("Alice", operation.value);
},
operation =>
{
Assert.Equal("replace", operation.op);
Assert.Equal("/Age", operation.path);
Assert.Equal(42L, operation.value);
},
operation =>
{
Assert.Equal("remove", operation.op);
Assert.Equal("/Name", operation.path);
},
operation =>
{
Assert.Equal("test", operation.op);
Assert.Equal("/Age", operation.path);
Assert.Equal(42L, operation.value);
});
}

[Fact]
public void Map_Maps_To_Target_Member_With_Different_Name()
{
var mapper = new global::Mapper.Mapper(new RenameProfile());
var patch = CreatePatch<SourceModel>(Operation<SourceModel>("replace", "/Name", value: "Bob"));

var mapped = mapper.Map<SourceModel, RenamedTargetModel>(patch);

var operation = Assert.Single(mapped.Operations);
Assert.Equal("/DisplayName", operation.path);
Assert.Equal("Bob", operation.value);
}

[Fact]
public void Map_Converts_Value_When_Target_Type_Differs()
{
var mapper = new global::Mapper.Mapper(new ComputedProfile());
var patch = CreatePatch<SourceStatusModel>(Operation<SourceStatusModel>("replace", "/Status", value: "Active"));

var mapped = mapper.Map<SourceStatusModel, TargetStatusModel>(patch);

var operation = Assert.Single(mapped.Operations);
Assert.Equal("/Status", operation.path);
Assert.Equal(TargetStatus.Active, operation.value);
}

[Fact]
public void Map_Skips_Ignored_Target_Member()
{
var mapper = new global::Mapper.Mapper(new IgnoreProfile());
var patch = CreatePatch<SourceModel>(Operation<SourceModel>("replace", "/Name", value: "Ignored"));

var mapped = mapper.Map<SourceModel, TargetModel>(patch);

Assert.Empty(mapped.Operations);
}

private static JsonPatchDocument<TModel> CreatePatch<TModel>(params Operation<TModel>[] operations)
where TModel : class
{
var patch = new JsonPatchDocument<TModel>();
foreach (var operation in operations)
{
patch.Operations.Add(operation);
}

return patch;
}

private static Operation<TModel> Operation<TModel>(string op, string path, string? from = null, object? value = null)
where TModel : class =>
new()
{
op = op,
path = path,
from = from,
value = value
};

private sealed class DefaultProfile : global::Mapper.MapProfile
{
public DefaultProfile()
{
CreateMap<SourceModel, TargetModel>();
}
}

private sealed class RenameProfile : global::Mapper.MapProfile
{
public RenameProfile()
{
CreateMap<SourceModel, RenamedTargetModel>()
.ForMember(target => target.DisplayName, options => options.MapFrom(source => source.Name));
}
}

private sealed class ComputedProfile : global::Mapper.MapProfile
{
public ComputedProfile()
{
CreateMap<SourceStatusModel, TargetStatusModel>()
.ForMember(
target => target.Status,
options => options.MapFrom<TargetStatus?>(source => string.IsNullOrWhiteSpace(source.Status)
? null
: Enum.Parse<TargetStatus>(source.Status, true)));
}
}

private sealed class IgnoreProfile : global::Mapper.MapProfile
{
public IgnoreProfile()
{
CreateMap<SourceModel, TargetModel>()
.ForMember(target => target.Name, options => options.Ignore());
}
}

private sealed class SourceModel
{
public string? Name { get; set; }

public int Age { get; set; }
}

private sealed class TargetModel
{
public string? Name { get; set; }

public long Age { get; set; }
}

private sealed class RenamedTargetModel
{
public string? DisplayName { get; set; }
}

private sealed class SourceStatusModel
{
public string? Status { get; set; }
}

private sealed class TargetStatusModel
{
public TargetStatus? Status { get; set; }
}

private enum TargetStatus
{
Unknown = 0,
Active = 1
}
}
27 changes: 27 additions & 0 deletions src/Mapper.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapper", "Mapper/Mapper.csproj", "{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapper.Tests", "Mapper.Tests/Mapper.Tests.csproj", "{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FA4B596-73B9-4C77-8B7C-C6E1449BE3EB}.Release|Any CPU.Build.0 = Release|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB54888B-6954-449C-8FF4-0A1F95A4F4C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Loading