feat(core): DocumentService — create/open/save/close (API7)
- KompasDocumentType (Part/Assembly/Drawing/Fragment/Specification/Text) + маппинг в DocumentTypeEnum и парсинг строк (ru/en) - DocumentInfo (Name/PathName/Type) - DocumentService: Create/Open/Save/SaveAs/Close/GetActive через IDocuments - tests: интеграция документов (create part/fragment, SaveAs .scratch, close) — зелёные Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
namespace Kompas.Mcp.Core.Documents;
|
||||
|
||||
/// <summary>Сводка по документу КОМПАС.</summary>
|
||||
public sealed record DocumentInfo
|
||||
{
|
||||
/// <summary>Имя файла без пути (или имя по умолчанию для несохранённого).</summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>Полный путь файла; пусто, если документ ещё не сохранён.</summary>
|
||||
public required string PathName { get; init; }
|
||||
|
||||
/// <summary>Тип документа.</summary>
|
||||
public required KompasDocumentType Type { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6Constants;
|
||||
using KompasAPI7;
|
||||
|
||||
namespace Kompas.Mcp.Core.Documents;
|
||||
|
||||
/// <summary>Жизненный цикл документов КОМПАС через API7 (<see cref="IDocuments"/>).</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class DocumentService
|
||||
{
|
||||
private readonly KompasSession _session;
|
||||
private readonly KompasDispatcher _dispatcher;
|
||||
|
||||
public DocumentService(KompasSession session, KompasDispatcher dispatcher)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
}
|
||||
|
||||
/// <summary>Создать новый документ заданного типа и сделать его активным.</summary>
|
||||
public Task<DocumentInfo> CreateAsync(KompasDocumentType type, bool visible = true, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc = (IKompasDocument)_session.Application.Documents.Add(DocumentTypes.ToEnum(type), visible);
|
||||
return ToInfo(doc);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Открыть документ с диска.</summary>
|
||||
public Task<DocumentInfo> OpenAsync(string path, bool readOnly = false, bool visible = true, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc = (IKompasDocument)_session.Application.Documents.Open(path, visible, readOnly);
|
||||
if (doc is null)
|
||||
throw new InvalidOperationException($"Не удалось открыть документ: {path}");
|
||||
return ToInfo(doc);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Сводка по активному документу или <c>null</c>, если документов нет.</summary>
|
||||
public Task<DocumentInfo?> GetActiveAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc = _session.Application.ActiveDocument;
|
||||
return doc is null ? null : ToInfo(doc);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Сохранить активный документ (для уже сохранённого — без диалога).</summary>
|
||||
public Task SaveAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => RequireActive().Save(), ct);
|
||||
|
||||
/// <summary>Сохранить активный документ по указанному пути (безопасно для новых документов).</summary>
|
||||
public Task SaveAsAsync(string path, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => RequireActive().SaveAs(path), ct);
|
||||
|
||||
/// <summary>Закрыть активный документ. <paramref name="save"/> — сохранять ли изменения.</summary>
|
||||
public Task<bool> CloseAsync(bool save = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
RequireActive().Close(save ? DocumentCloseOptions.kdSaveChanges : DocumentCloseOptions.kdDoNotSaveChanges), ct);
|
||||
|
||||
private IKompasDocument RequireActive()
|
||||
=> _session.Application.ActiveDocument
|
||||
?? throw new InvalidOperationException("Нет активного документа.");
|
||||
|
||||
private static DocumentInfo ToInfo(IKompasDocument doc) => new()
|
||||
{
|
||||
Name = doc.Name ?? string.Empty,
|
||||
PathName = doc.PathName ?? string.Empty,
|
||||
Type = DocumentTypes.FromEnum(doc.DocumentType),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Kompas6Constants;
|
||||
|
||||
namespace Kompas.Mcp.Core.Documents;
|
||||
|
||||
/// <summary>Дружелюбный тип документа КОМПАС (обёртка над <see cref="DocumentTypeEnum"/>).</summary>
|
||||
public enum KompasDocumentType
|
||||
{
|
||||
Part,
|
||||
Assembly,
|
||||
Drawing,
|
||||
Fragment,
|
||||
Specification,
|
||||
Text,
|
||||
}
|
||||
|
||||
/// <summary>Маппинг дружелюбного типа документа ↔ перечисление API7 ↔ строка.</summary>
|
||||
public static class DocumentTypes
|
||||
{
|
||||
public static DocumentTypeEnum ToEnum(KompasDocumentType type) => type switch
|
||||
{
|
||||
KompasDocumentType.Part => DocumentTypeEnum.ksDocumentPart,
|
||||
KompasDocumentType.Assembly => DocumentTypeEnum.ksDocumentAssembly,
|
||||
KompasDocumentType.Drawing => DocumentTypeEnum.ksDocumentDrawing,
|
||||
KompasDocumentType.Fragment => DocumentTypeEnum.ksDocumentFragment,
|
||||
KompasDocumentType.Specification => DocumentTypeEnum.ksDocumentSpecification,
|
||||
KompasDocumentType.Text => DocumentTypeEnum.ksDocumentTextual,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип документа."),
|
||||
};
|
||||
|
||||
public static KompasDocumentType FromEnum(DocumentTypeEnum type) => type switch
|
||||
{
|
||||
DocumentTypeEnum.ksDocumentPart => KompasDocumentType.Part,
|
||||
DocumentTypeEnum.ksDocumentAssembly => KompasDocumentType.Assembly,
|
||||
DocumentTypeEnum.ksDocumentDrawing => KompasDocumentType.Drawing,
|
||||
DocumentTypeEnum.ksDocumentFragment => KompasDocumentType.Fragment,
|
||||
DocumentTypeEnum.ksDocumentSpecification => KompasDocumentType.Specification,
|
||||
DocumentTypeEnum.ksDocumentTextual => KompasDocumentType.Text,
|
||||
_ => KompasDocumentType.Part,
|
||||
};
|
||||
|
||||
/// <summary>Разобрать строку ("part", "assembly", "drawing", "fragment", ...) в тип.</summary>
|
||||
public static KompasDocumentType Parse(string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
return value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"part" or "деталь" => KompasDocumentType.Part,
|
||||
"assembly" or "сборка" => KompasDocumentType.Assembly,
|
||||
"drawing" or "чертеж" or "чертёж" => KompasDocumentType.Drawing,
|
||||
"fragment" or "фрагмент" => KompasDocumentType.Fragment,
|
||||
"specification" or "спецификация" => KompasDocumentType.Specification,
|
||||
"text" or "txt" or "текст" => KompasDocumentType.Text,
|
||||
_ => throw new ArgumentException(
|
||||
$"Неизвестный тип документа '{value}'. Допустимо: part, assembly, drawing, fragment, specification, text.",
|
||||
nameof(value)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: жизненный цикл документов.</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class DocumentTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
|
||||
public DocumentTests(KompasFixture fx)
|
||||
=> _docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||
|
||||
[Fact]
|
||||
public async Task Create_part_saveas_close_roundtrip()
|
||||
{
|
||||
var created = await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
Assert.Equal(KompasDocumentType.Part, created.Type);
|
||||
|
||||
var path = TestPaths.NewFile(".m3d");
|
||||
await _docs.SaveAsAsync(path);
|
||||
|
||||
var active = await _docs.GetActiveAsync();
|
||||
Assert.NotNull(active);
|
||||
Assert.Equal(path, active!.PathName, ignoreCase: true);
|
||||
|
||||
var closed = await _docs.CloseAsync(save: false);
|
||||
Assert.True(closed);
|
||||
Assert.True(File.Exists(path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_fragment_reports_type()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Fragment);
|
||||
var active = await _docs.GetActiveAsync();
|
||||
Assert.NotNull(active);
|
||||
Assert.Equal(KompasDocumentType.Fragment, active!.Type);
|
||||
await _docs.CloseAsync(save: false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
/// <summary>Пути для тестовых артефактов: корень репозитория и gitignore-папка .scratch.</summary>
|
||||
internal static class TestPaths
|
||||
{
|
||||
public static string RepoRoot()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "KompasMcp.slnx")))
|
||||
dir = dir.Parent;
|
||||
return dir?.FullName ?? AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
public static string Scratch
|
||||
=> Directory.CreateDirectory(Path.Combine(RepoRoot(), ".scratch")).FullName;
|
||||
|
||||
public static string NewFile(string extension)
|
||||
=> Path.Combine(Scratch, $"test_{DateTime.Now:HHmmss}_{Guid.NewGuid():N}{extension}");
|
||||
}
|
||||
Reference in New Issue
Block a user