Skip to content

Commit dcf2223

Browse files
committed
Refactor embedded resources API
1 parent 4807f45 commit dcf2223

6 files changed

Lines changed: 295 additions & 172 deletions

File tree

src/CoreKit/Extensions/AssemblyExtensions.cs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright AppMotor Framework (https://github.com/skrysm/AppMotor)
33

44
using System.Reflection;
5+
using System.Resources;
56

67
using AppMotor.CoreKit.Exceptions;
78
using AppMotor.CoreKit.Utils;
@@ -50,11 +51,69 @@ public static string GetDefaultNamespace(this Assembly assembly)
5051
}
5152

5253
/// <summary>
53-
/// Returns an "accessor" to the embedded resources of this assembly.
54+
/// Returns whether the specified embedded resource exists in this assembly.
5455
/// </summary>
56+
/// <param name="assembly">this assembly</param>
57+
/// <param name="resourceName">The full resource name (case-sensitive).</param>
5558
[MustUseReturnValue]
56-
public static AssemblyEmbeddedResources GetEmbeddedResources(this Assembly assembly)
59+
public static bool DoesEmbeddedResourceExist(this Assembly assembly, string resourceName)
5760
{
58-
return new AssemblyEmbeddedResources(assembly);
61+
return assembly.GetManifestResourceInfo(resourceName) is not null;
62+
}
63+
64+
/// <summary>
65+
/// Returns an "accessor" to the specified embedded resource of this assembly. Throws a <see cref="MissingManifestResourceException"/>
66+
/// if the resource doesn't exist.
67+
/// </summary>
68+
/// <param name="assembly">this assembly</param>
69+
/// <param name="resourceName">The full resource name (case-sensitive).</param>
70+
[MustUseReturnValue]
71+
public static EmbeddedResource GetEmbeddedResource(this Assembly assembly, string resourceName)
72+
{
73+
return assembly.GetEmbeddedResourceOrNull(resourceName)
74+
?? throw new MissingManifestResourceException($"The embedded resource '{resourceName}' was not found in assembly '{assembly.GetSimpleName()}'.");
75+
}
76+
77+
/// <summary>
78+
/// Returns an "accessor" to the specified embedded resource of this assembly. Throws a <see cref="MissingManifestResourceException"/>
79+
/// if the resource doesn't exist.
80+
/// </summary>
81+
/// <param name="assembly">this assembly</param>
82+
/// <param name="type">The type whose namespace is used to scope the <paramref name="resourceName"/>.</param>
83+
/// <param name="resourceName">The resource name (case-sensitive).</param>
84+
[MustUseReturnValue]
85+
public static EmbeddedResource GetEmbeddedResource(this Assembly assembly, Type type, string resourceName)
86+
{
87+
return assembly.GetEmbeddedResource(EmbeddedResource.ConstructResourceName(type, resourceName));
88+
}
89+
90+
/// <summary>
91+
/// Returns an "accessor" to the specified embedded resource of this assembly. Returns <c>null</c> if
92+
/// the resource doesn't exist.
93+
/// </summary>
94+
/// <param name="assembly">this assembly</param>
95+
/// <param name="resourceName">The full resource name (case-sensitive).</param>
96+
[MustUseReturnValue]
97+
public static EmbeddedResource? GetEmbeddedResourceOrNull(this Assembly assembly, string resourceName)
98+
{
99+
if (!assembly.DoesEmbeddedResourceExist(resourceName))
100+
{
101+
return null;
102+
}
103+
104+
return new EmbeddedResource(assembly, resourceName);
105+
}
106+
107+
/// <summary>
108+
/// Returns an "accessor" to the specified embedded resource of this assembly. Returns <c>null</c> if
109+
/// the resource doesn't exist.
110+
/// </summary>
111+
/// <param name="assembly">this assembly</param>
112+
/// <param name="type">The type whose namespace is used to scope the <paramref name="resourceName"/>.</param>
113+
/// <param name="resourceName">The resource name (case-sensitive).</param>
114+
[MustUseReturnValue]
115+
public static EmbeddedResource? GetEmbeddedResourceOrNull(this Assembly assembly, Type type, string resourceName)
116+
{
117+
return assembly.GetEmbeddedResourceOrNull(EmbeddedResource.ConstructResourceName(type, resourceName));
59118
}
60119
}

src/CoreKit/Utils/AssemblyEmbeddedResources.cs

Lines changed: 0 additions & 88 deletions
This file was deleted.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright AppMotor Framework (https://github.com/skrysm/AppMotor)
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Reflection;
6+
using System.Text;
7+
8+
using AppMotor.CoreKit.Exceptions;
9+
using AppMotor.CoreKit.Extensions;
10+
11+
using JetBrains.Annotations;
12+
13+
namespace AppMotor.CoreKit.Utils;
14+
15+
/// <summary>
16+
/// Provides an "accessor" to an embedded resource of an assembly.
17+
/// </summary>
18+
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "No really a data class")]
19+
public readonly struct EmbeddedResource
20+
{
21+
private readonly Assembly _assembly;
22+
23+
private readonly string _resourceName;
24+
25+
/// <summary>
26+
/// Constructor for existing resource.
27+
/// </summary>
28+
internal EmbeddedResource(Assembly assembly, string resourceName)
29+
{
30+
Validate.ArgumentWithName(nameof(assembly)).IsNotNull(assembly);
31+
Validate.ArgumentWithName(nameof(resourceName)).IsNotNullOrWhiteSpace(resourceName);
32+
33+
this._assembly = assembly;
34+
this._resourceName = resourceName;
35+
}
36+
37+
/// <summary>
38+
/// Constructs the resource name from the type's namespace and the resource name.
39+
/// </summary>
40+
/// <param name="type">The type whose namespace is used to scope the <paramref name="resourceName"/>.</param>
41+
/// <param name="resourceName">The resource name (case-sensitive).</param>
42+
[MustUseReturnValue]
43+
public static string ConstructResourceName(Type type, string resourceName)
44+
{
45+
var nameSpace = type.Namespace;
46+
47+
char c = Type.Delimiter;
48+
49+
return string.Concat(nameSpace, new ReadOnlySpan<char>(in c), resourceName);
50+
}
51+
52+
/// <summary>
53+
/// Returns a <see cref="Stream"/> for this resource.
54+
/// </summary>
55+
[MustUseReturnValue, MustDisposeResource]
56+
public Stream AsStream()
57+
{
58+
// NOTE: We throw an UnexpectedBehaviorException() here because the premise of this type is that the resource exists.
59+
return this._assembly.GetManifestResourceStream(this._resourceName)
60+
?? throw new UnexpectedBehaviorException($"The embedded resource '{this._resourceName}' doesn't exist in assembly '{this._assembly.GetSimpleName()}'.");
61+
}
62+
63+
/// <summary>
64+
/// Returns this resource as byte array.
65+
/// </summary>
66+
/// <returns></returns>
67+
[MustUseReturnValue]
68+
public byte[] AsBytes()
69+
{
70+
using var stream = AsStream();
71+
return stream.ReadAsBytes();
72+
}
73+
74+
/// <summary>
75+
/// Returns this resource as string.
76+
/// </summary>
77+
[MustUseReturnValue]
78+
public string AsString(Encoding encoding)
79+
{
80+
using var stream = AsStream();
81+
return stream.ReadAsString(encoding);
82+
}
83+
84+
/// <summary>
85+
/// Returns this resource as lines.
86+
/// </summary>
87+
[MustUseReturnValue]
88+
public IEnumerable<string> AsLines(Encoding encoding)
89+
{
90+
// NOTE: This is generally safe as this line will only be called when "IEnumerable<string>.GetEnumerator()" is called -
91+
// NOT when this method is called. (It's the C# iterator magic.)
92+
using var stream = AsStream();
93+
94+
// IMPORTANT: We must NOT return the result of "ReadAsLine()" directly here because
95+
// we need to make sure the stream is only disposed after all (required) lines have
96+
// been read.
97+
foreach (var line in stream.ReadAsLines(encoding))
98+
{
99+
yield return line;
100+
}
101+
}
102+
}

tests/CoreKit/Tests/Extensions/AssemblyExtensionsTests.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Copyright AppMotor Framework (https://github.com/skrysm/AppMotor)
33

44
using System.Reflection;
5+
using System.Resources;
56

67
using AppMotor.CoreKit.Extensions;
8+
using AppMotor.CoreKit.TestData;
79

810
using Shouldly;
911

@@ -16,9 +18,50 @@ namespace AppMotor.CoreKit.Tests.Extensions;
1618
/// </summary>
1719
public sealed class AssemblyExtensionsTests
1820
{
21+
private static Assembly ThisAssembly => Assembly.GetExecutingAssembly();
22+
1923
[Fact]
2024
public void Test_GetSimpleName()
2125
{
22-
Assembly.GetExecutingAssembly().GetSimpleName().ShouldBe("AppMotor.CoreKit.Tests");
26+
ThisAssembly.GetSimpleName().ShouldBe("AppMotor.CoreKit.Tests");
27+
}
28+
29+
[Fact]
30+
public void Test_DoesEmbeddedResourceExist()
31+
{
32+
ThisAssembly.DoesEmbeddedResourceExist("TestData/StreamReadTest.txt").ShouldBe(true);
33+
ThisAssembly.DoesEmbeddedResourceExist("I-do-not-exist.txt").ShouldBe(false);
34+
}
35+
36+
[Fact]
37+
public void Test_GetEmbeddedResource_NameOnly()
38+
{
39+
Should.NotThrow(() => ThisAssembly.GetEmbeddedResource("TestData/StreamReadTest.txt"));
40+
41+
var ex = Should.Throw<MissingManifestResourceException>(() => ThisAssembly.GetEmbeddedResource("I-do-not-exist.txt"));
42+
ex.Message.ShouldBe("The embedded resource 'I-do-not-exist.txt' was not found in assembly 'AppMotor.CoreKit.Tests'.");
43+
}
44+
45+
[Fact]
46+
public void Test_GetEmbeddedResource_TypeAndName()
47+
{
48+
Should.NotThrow(() => ThisAssembly.GetEmbeddedResource(typeof(ResourceLocatorType), "AnotherFile.txt"));
49+
50+
var ex = Should.Throw<MissingManifestResourceException>(() => ThisAssembly.GetEmbeddedResource(typeof(ResourceLocatorType), "I-do-not-exist.txt"));
51+
ex.Message.ShouldBe("The embedded resource 'AppMotor.CoreKit.TestData.I-do-not-exist.txt' was not found in assembly 'AppMotor.CoreKit.Tests'.");
52+
}
53+
54+
[Fact]
55+
public void Test_GetEmbeddedResourceOrNull_NameOnly()
56+
{
57+
ThisAssembly.GetEmbeddedResourceOrNull("TestData/StreamReadTest.txt").ShouldNotBeNull();
58+
ThisAssembly.GetEmbeddedResourceOrNull("I-do-not-exist.txt").ShouldBe(null);
59+
}
60+
61+
[Fact]
62+
public void Test_GetEmbeddedResourceOrNull_TypeAndName()
63+
{
64+
ThisAssembly.GetEmbeddedResourceOrNull(typeof(ResourceLocatorType), "AnotherFile.txt").ShouldNotBeNull();
65+
ThisAssembly.GetEmbeddedResourceOrNull(typeof(ResourceLocatorType), "I-do-not-exist.txt").ShouldBe(null);
2366
}
2467
}

0 commit comments

Comments
 (0)