Skip to content

Commit b347c59

Browse files
committed
improve Imports
1 parent ea3cd82 commit b347c59

File tree

3 files changed

+240
-1
lines changed

3 files changed

+240
-1
lines changed

src/FluentCommand.Import/ImportDefinition.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text.Json.Serialization;
2+
using System.Text.RegularExpressions;
23

34
namespace FluentCommand.Import;
45

@@ -73,7 +74,6 @@ public class ImportDefinition
7374
[Obsolete("Use ValidatorKey instead. This property will be removed in a future version.")]
7475
public Type? Validator { get; set; }
7576

76-
7777
/// <summary>
7878
/// Gets or sets the service key used to resolve the <see cref="IImportValidator"/> service from the dependency injection container.
7979
/// The service must be registered in the DI container with this key and implement <see cref="IImportValidator"/>.
@@ -84,6 +84,63 @@ public class ImportDefinition
8484
[JsonPropertyName("validatorKey")]
8585
public string? ValidatorKey { get; set; }
8686

87+
/// <summary>
88+
/// Builds a mapping between import fields and source headers using regular expressions defined in <see cref="Fields"/>.
89+
/// </summary>
90+
/// <param name="headers">The list of source headers to match against field expressions.</param>
91+
/// <returns>
92+
/// A list of <see cref="FieldMap"/> objects representing the mapping between import fields and source headers.
93+
/// </returns>
94+
public List<FieldMap> BuildMapping(IReadOnlyCollection<FieldMap>? headers)
95+
{
96+
var list = new List<FieldMap>();
97+
98+
// create a mapping for all fields
99+
foreach (var field in Fields)
100+
{
101+
// Skip fields that cannot be mapped
102+
if (!field.CanMap)
103+
continue;
104+
105+
var map = new FieldMap { Name = field.Name };
106+
list.Add(map);
107+
108+
// no expression, don't set mapped index
109+
if (field.Expressions == null || field.Expressions.Count == 0
110+
|| headers == null || headers.Count == 0)
111+
{
112+
continue;
113+
}
114+
115+
// for each expression, try to match against headers
116+
foreach (var expression in field.Expressions)
117+
{
118+
foreach (var header in headers)
119+
{
120+
try
121+
{
122+
if (Regex.IsMatch(header.Name, expression))
123+
{
124+
map.Index = header.Index;
125+
break; // Stop after the first match
126+
}
127+
}
128+
catch
129+
{
130+
// skip error
131+
}
132+
}
133+
134+
// Stop checking expressions if we found a match
135+
if (map.Index.HasValue)
136+
break;
137+
}
138+
}
139+
140+
return list;
141+
142+
}
143+
87144
/// <summary>
88145
/// Builds an <see cref="ImportDefinition"/> using the specified builder action.
89146
/// </summary>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.ComponentModel.DataAnnotations.Schema;
3+
4+
using FluentCommand.Import;
5+
6+
namespace FluentCommand.SqlServer.Tests;
7+
8+
public class ImportDefinitionBuilderTests
9+
{
10+
[Table("TestModel", Schema = "UT")]
11+
private class TestModel
12+
{
13+
[Key]
14+
public int Id { get; set; }
15+
public string Name { get; set; } = "";
16+
public bool IsActive { get; set; }
17+
18+
[Column("Created", TypeName = "datetime2")]
19+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
20+
21+
[NotMapped]
22+
public string NotMappedProperty { get; set; } = "";
23+
}
24+
25+
[Fact]
26+
public void AutoMap_Should_Map_All_Eligible_Properties()
27+
{
28+
var definition = ImportDefinitionBuilder<TestModel>.Build(b => b.AutoMap());
29+
30+
definition.Should().NotBeNull();
31+
definition.Name.Should().Be("TestModel");
32+
definition.TargetTable.Should().Be("UT.TestModel");
33+
definition.Fields.Should().Contain(f => f.Name == nameof(TestModel.Id));
34+
definition.Fields.Should().Contain(f => f.Name == nameof(TestModel.Name));
35+
definition.Fields.Should().Contain(f => f.Name == nameof(TestModel.IsActive));
36+
definition.Fields.Should().Contain(f => f.Name == "Created");
37+
definition.Fields.Should().NotContain(f => f.Name == nameof(TestModel.NotMappedProperty));
38+
39+
definition.Fields.First(f => f.Name == nameof(TestModel.Id)).IsKey.Should().BeTrue();
40+
}
41+
42+
[Fact]
43+
public void Field_Should_Allow_Explicit_Field_Configuration()
44+
{
45+
var definition = ImportDefinitionBuilder<TestModel>.Build(b => b
46+
.Field(m => m.Name)
47+
.DisplayName("Custom Name")
48+
.Required()
49+
);
50+
51+
var field = definition.Fields.FirstOrDefault(f => f.Name == nameof(TestModel.Name));
52+
field.Should().NotBeNull();
53+
field.DisplayName.Should().Be("Custom Name");
54+
field.IsRequired.Should().BeTrue();
55+
}
56+
57+
[Fact]
58+
public void Name_And_TargetTable_Should_Set_Properties()
59+
{
60+
var definition = ImportDefinitionBuilder<TestModel>.Build(b => b
61+
.Name("MyImport")
62+
.TargetTable("dbo.MyTable")
63+
);
64+
65+
definition.Name.Should().Be("MyImport");
66+
definition.TargetTable.Should().Be("dbo.MyTable");
67+
}
68+
69+
[Fact]
70+
public void CanInsert_And_CanUpdate_Should_Set_Properties()
71+
{
72+
var definition = ImportDefinitionBuilder<TestModel>.Build(b => b
73+
.CanInsert(false)
74+
.CanUpdate(false)
75+
);
76+
77+
definition.CanInsert.Should().BeFalse();
78+
definition.CanUpdate.Should().BeFalse();
79+
}
80+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using FluentCommand.Import;
2+
3+
namespace FluentCommand.SqlServer.Tests;
4+
5+
public class ImportDefinitionTests
6+
{
7+
[Fact]
8+
public void Build_Should_Throw_On_Null_Builder()
9+
{
10+
Action act = () => ImportDefinition.Build(null);
11+
act.Should().Throw<ArgumentNullException>();
12+
}
13+
14+
[Fact]
15+
public void Build_Should_Configure_Properties()
16+
{
17+
var definition = ImportDefinition.Build(b => b
18+
.Name("TestImport")
19+
.TargetTable("TestTable")
20+
.CanInsert(false)
21+
.CanUpdate(true)
22+
.MaxErrors(5)
23+
.Field(f => f
24+
.FieldName("Field1")
25+
.DataType<string>()
26+
.CanMap()
27+
)
28+
);
29+
30+
definition.Name.Should().Be("TestImport");
31+
definition.TargetTable.Should().Be("TestTable");
32+
definition.CanInsert.Should().BeFalse();
33+
definition.CanUpdate.Should().BeTrue();
34+
definition.MaxErrors.Should().Be(5);
35+
definition.Fields.Should().ContainSingle(f => f.Name == "Field1");
36+
}
37+
38+
[Fact]
39+
public void BuildMapping_Should_Map_Fields_By_Regex()
40+
{
41+
var definition = new ImportDefinition
42+
{
43+
Fields =
44+
[
45+
new FieldDefinition
46+
{
47+
Name = "Email",
48+
CanMap = true,
49+
Expressions = ["^email$", "e-mail"]
50+
},
51+
new FieldDefinition
52+
{
53+
Name = "Name",
54+
CanMap = true,
55+
Expressions = ["^name$"]
56+
},
57+
new FieldDefinition
58+
{
59+
Name = "Ignored",
60+
CanMap = false
61+
}
62+
]
63+
};
64+
65+
var headers = new List<FieldMap>
66+
{
67+
new() { Name = "email", Index = 0 },
68+
new() { Name = "name", Index = 1 }
69+
};
70+
71+
var result = definition.BuildMapping(headers);
72+
73+
result.Should().HaveCount(2);
74+
result[0].Name.Should().Be("Email");
75+
result[0].Index.Should().Be(0);
76+
result[1].Name.Should().Be("Name");
77+
result[1].Index.Should().Be(1);
78+
}
79+
80+
[Fact]
81+
public void BuildMapping_Should_Handle_Null_Or_Empty_Headers()
82+
{
83+
var definition = new ImportDefinition
84+
{
85+
Fields =
86+
[
87+
new FieldDefinition
88+
{
89+
Name = "Field1",
90+
CanMap = true,
91+
Expressions = [".*"]
92+
}
93+
]
94+
};
95+
96+
var result1 = definition.BuildMapping(null);
97+
var result2 = definition.BuildMapping([]);
98+
99+
result1.Should().ContainSingle(f => f.Name == "Field1" && f.Index == null);
100+
result2.Should().ContainSingle(f => f.Name == "Field1" && f.Index == null);
101+
}
102+
}

0 commit comments

Comments
 (0)