mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Use generated internal interfaces for DI services
This commit is contained in:
@@ -144,6 +144,10 @@ Pure.DI is a private build-time dependency; consumers
|
||||
do not need a container or a new client API. The default graph reuses workflow services, not
|
||||
opened archives, readers, writers, or streams, which retain their existing ownership rules.
|
||||
Services are exposed directly as typed composition roots, each a singleton within its composition.
|
||||
Roots and constructor dependencies use internal interfaces generated by Pure.DI's
|
||||
`[GenerateInterface(asInternal: true)]`. `Services/ServiceContracts.cs` declares their identities
|
||||
for composition binding and supplements the sync signatures produced by Zomp, since source
|
||||
generators cannot inspect one another's generated members.
|
||||
Public extension classes remain beside their public abstractions. Simple overloads supply defaults
|
||||
locally; extraction and filesystem workflows delegate to `ArchiveExtractor` and `ArchiveFileWriter`,
|
||||
including reader extraction, writable-archive entry creation, and saving archives to files.
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace SharpCompress.Archives;
|
||||
|
||||
public static partial class ArchiveFactory
|
||||
{
|
||||
private static ArchiveService Service => LibraryDefaults.Composition.Archives;
|
||||
private static IArchiveService Service => LibraryDefaults.Composition.Archives;
|
||||
|
||||
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
|
||||
Service.OpenArchive(stream, readerOptions);
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace SharpCompress.Archives;
|
||||
public static partial class IArchiveEntryExtensions
|
||||
{
|
||||
// Give the sync generator a known receiver type before Pure.DI generates the root property.
|
||||
private static ArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
|
||||
private static IArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
|
||||
|
||||
/// <param name="archiveEntry">The archive entry to extract.</param>
|
||||
extension(IArchiveEntry archiveEntry)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
@@ -9,7 +10,8 @@ using SharpCompress.IO;
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Destination streams are borrowed, not owned by the reusable extraction service.
|
||||
internal sealed partial class ArchiveExtractor
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveExtractor : IArchiveExtractor
|
||||
{
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
|
||||
public async ValueTask WriteToAsync(
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ArchiveFileWriter
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveFileWriter : IArchiveFileWriter
|
||||
{
|
||||
public void Write(IWriter writer, string entryPath, FileInfo source)
|
||||
{
|
||||
|
||||
@@ -88,7 +88,7 @@ internal sealed partial class ArchiveService
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal ValueTask<T> FindFactoryAsync<T>(
|
||||
public ValueTask<T> FindFactoryAsync<T>(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@@ -98,7 +98,7 @@ internal sealed partial class ArchiveService
|
||||
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
|
||||
}
|
||||
|
||||
internal async ValueTask<T> FindFactoryAsync<T>(
|
||||
public async ValueTask<T> FindFactoryAsync<T>(
|
||||
FileInfo fileInfo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@@ -123,7 +123,7 @@ internal sealed partial class ArchiveService
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async ValueTask<T> FindFactoryAsync<T>(
|
||||
public async ValueTask<T> FindFactoryAsync<T>(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@@ -181,7 +181,8 @@ internal sealed partial class ArchiveService
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var factory in catalog.Factories)
|
||||
// The explicit type lets Zomp resolve probe overloads before Pure.DI emits catalog members.
|
||||
foreach (IFactory factory in catalog.Factories)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
var isArchive = await factory
|
||||
@@ -279,7 +280,7 @@ internal sealed partial class ArchiveService
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var factory in catalog.Factories)
|
||||
foreach (IFactory factory in catalog.Factories)
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
if (
|
||||
@@ -334,7 +335,7 @@ internal sealed partial class ArchiveService
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
foreach (var wrapper in catalog.Wrappers)
|
||||
foreach (TarWrapper wrapper in catalog.Wrappers)
|
||||
{
|
||||
#if !SYNC_ONLY
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Options;
|
||||
@@ -12,11 +13,12 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ArchiveService(
|
||||
FactoryCatalog catalog,
|
||||
ReaderService readers,
|
||||
ArchiveExtractor extractor
|
||||
)
|
||||
IFactoryCatalog catalog,
|
||||
IReaderService readers,
|
||||
IArchiveExtractor extractor
|
||||
) : IArchiveService
|
||||
{
|
||||
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Factories;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Read through on each operation: legacy registrations made after composition must remain visible.
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed class FactoryCatalog(
|
||||
Func<IEnumerable<IFactory>> factories,
|
||||
Func<IEnumerable<TarWrapper>> wrappers
|
||||
)
|
||||
) : IFactoryCatalog
|
||||
{
|
||||
public IEnumerable<IFactory> Factories => factories();
|
||||
public IEnumerable<TarWrapper> Wrappers => wrappers();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.IO;
|
||||
@@ -8,7 +9,8 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed partial class ReaderService(FactoryCatalog catalog)
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed partial class ReaderService(IFactoryCatalog catalog) : IReaderService
|
||||
{
|
||||
public IReader OpenReader(string filePath, ReaderOptions? options = null)
|
||||
{
|
||||
|
||||
@@ -9,27 +9,27 @@ internal partial class ServiceComposition
|
||||
private static void Setup() =>
|
||||
DI.Setup()
|
||||
.Hint(Hint.Resolve, "Off")
|
||||
.Arg<FactoryCatalog>("catalog")
|
||||
.Bind()
|
||||
.Arg<IFactoryCatalog>("catalog")
|
||||
.Bind<IReaderService>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ReaderService>()
|
||||
.Bind()
|
||||
.Bind<IArchiveService>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveService>()
|
||||
.Bind()
|
||||
.Bind<IWriterService>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<WriterService>()
|
||||
.Bind()
|
||||
.Bind<IArchiveExtractor>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveExtractor>()
|
||||
.Bind()
|
||||
.Bind<IArchiveFileWriter>()
|
||||
.As(Lifetime.Singleton)
|
||||
.To<ArchiveFileWriter>()
|
||||
.Root<ArchiveService>("Archives")
|
||||
.Root<ReaderService>("Readers")
|
||||
.Root<WriterService>("Writers")
|
||||
.Root<ArchiveExtractor>("Extractor")
|
||||
.Root<ArchiveFileWriter>("FileWriter");
|
||||
.Root<IArchiveService>("Archives")
|
||||
.Root<IReaderService>("Readers")
|
||||
.Root<IWriterService>("Writers")
|
||||
.Root<IArchiveExtractor>("Extractor")
|
||||
.Root<IArchiveFileWriter>("FileWriter");
|
||||
}
|
||||
|
||||
internal static class LibraryDefaults
|
||||
|
||||
62
src/SharpCompress/Services/ServiceContracts.cs
Normal file
62
src/SharpCompress/Services/ServiceContracts.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Detection;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
// Declare the contract identities up front so the composition can bind them in the same
|
||||
// generator pass that fills in their members.
|
||||
internal partial interface IFactoryCatalog;
|
||||
|
||||
internal partial interface IReaderService;
|
||||
|
||||
internal partial interface IWriterService;
|
||||
|
||||
internal partial interface IArchiveFileWriter;
|
||||
|
||||
// Pure.DI cannot see methods emitted by another source generator. Only the sync signatures
|
||||
// generated by Zomp are declared here; Pure.DI generates the rest from the implementations.
|
||||
internal partial interface IArchiveService
|
||||
{
|
||||
ArchiveDetection? DetectArchive(string filePath);
|
||||
ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions);
|
||||
ArchiveDetection? DetectArchive(Stream stream);
|
||||
ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions);
|
||||
ArchiveInformation? InspectArchive(string filePath);
|
||||
ArchiveInformation? InspectArchive(string filePath, ReaderOptions? readerOptions);
|
||||
ArchiveInformation? InspectArchive(Stream stream);
|
||||
ArchiveInformation? InspectArchive(Stream stream, ReaderOptions? readerOptions);
|
||||
ArchiveInformation? InspectArchive(
|
||||
IReadOnlyList<FileInfo> fileInfos,
|
||||
ReaderOptions? readerOptions = null
|
||||
);
|
||||
ArchiveInformation? InspectArchive(
|
||||
IReadOnlyList<Stream> streams,
|
||||
ReaderOptions? readerOptions = null
|
||||
);
|
||||
}
|
||||
|
||||
internal partial interface IArchiveExtractor
|
||||
{
|
||||
void WriteTo(
|
||||
IArchiveEntry archiveEntry,
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
ExtractionOptions? options = null,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
);
|
||||
void WriteToDirectory(
|
||||
IArchiveEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null
|
||||
);
|
||||
void WriteToFile(
|
||||
IArchiveEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pure.DI;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Options;
|
||||
using SharpCompress.Factories;
|
||||
@@ -10,7 +11,8 @@ using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Services;
|
||||
|
||||
internal sealed class WriterService(FactoryCatalog catalog)
|
||||
[GenerateInterface(asInternal: true)]
|
||||
internal sealed class WriterService(IFactoryCatalog catalog) : IWriterService
|
||||
{
|
||||
public IWriter OpenWriter(
|
||||
string filePath,
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -36,6 +37,12 @@ public class ServiceCompositionTests : TestBase
|
||||
typeof(WriterService),
|
||||
typeof(ArchiveExtractor),
|
||||
typeof(ArchiveFileWriter),
|
||||
typeof(IFactoryCatalog),
|
||||
typeof(IArchiveService),
|
||||
typeof(IReaderService),
|
||||
typeof(IWriterService),
|
||||
typeof(IArchiveExtractor),
|
||||
typeof(IArchiveFileWriter),
|
||||
];
|
||||
Assert.All(
|
||||
serviceTypes,
|
||||
@@ -52,6 +59,71 @@ public class ServiceCompositionTests : TestBase
|
||||
Assert.IsType<TarWrapper[]>(TarWrapper.Wrappers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceContracts_IncludeGeneratedSyncMethods()
|
||||
{
|
||||
(Type Contract, Type Implementation)[] services =
|
||||
[
|
||||
(typeof(IFactoryCatalog), typeof(FactoryCatalog)),
|
||||
(typeof(IArchiveService), typeof(ArchiveService)),
|
||||
(typeof(IReaderService), typeof(ReaderService)),
|
||||
(typeof(IWriterService), typeof(WriterService)),
|
||||
(typeof(IArchiveExtractor), typeof(ArchiveExtractor)),
|
||||
(typeof(IArchiveFileWriter), typeof(ArchiveFileWriter)),
|
||||
];
|
||||
foreach (var (contract, implementation) in services)
|
||||
{
|
||||
Assert.True(contract.IsAssignableFrom(implementation));
|
||||
foreach (
|
||||
var method in implementation.GetMethods(
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly
|
||||
)
|
||||
)
|
||||
{
|
||||
Assert.Contains(
|
||||
contract.GetMethods(),
|
||||
candidate => candidate.ToString() == method.ToString()
|
||||
);
|
||||
}
|
||||
}
|
||||
Assert.Equal(
|
||||
typeof(IArchiveService),
|
||||
typeof(ServiceComposition).GetProperty("Archives")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IReaderService),
|
||||
typeof(ServiceComposition).GetProperty("Readers")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IWriterService),
|
||||
typeof(ServiceComposition).GetProperty("Writers")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IArchiveExtractor),
|
||||
typeof(ServiceComposition).GetProperty("Extractor")!.PropertyType
|
||||
);
|
||||
Assert.Equal(
|
||||
typeof(IArchiveFileWriter),
|
||||
typeof(ServiceComposition).GetProperty("FileWriter")!.PropertyType
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Composition_AcceptsInterfaceOnlyCatalog()
|
||||
{
|
||||
IFactoryCatalog catalog = new TestFactoryCatalog();
|
||||
var composition = new ServiceComposition(catalog);
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
|
||||
Assert.Equal(ArchiveType.Zip, composition.Archives.DetectArchive(stream)?.ContainerType);
|
||||
Assert.Equal(0, stream.Position);
|
||||
}
|
||||
|
||||
private sealed class TestFactoryCatalog : IFactoryCatalog
|
||||
{
|
||||
public IEnumerable<IFactory> Factories { get; } = [new ZipFactory()];
|
||||
public IEnumerable<TarWrapper> Wrappers => TarWrapper.Wrappers;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Composition_ReusesServiceRootsDuringConcurrentAccess()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user