diff --git a/CLAUDE.md b/CLAUDE.md index e7cd336..d07197a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,10 @@ JSON, deserialized into `SplitterConfig` (case-insensitive). Sample: `example/pa - `rootPackageName` — *optional* root container package to scope the search to. Omit it when the packages sit directly under the model. +- `modelName` — *optional* name for the `uml:Model` wrapper. Defaults to the name of the model + enclosing the package in the source. Not used for `convertToLibrary` packages. - `packages[]` — `name`, `outputFile`, and optional `convertToLibrary` (default `false`): - - `false` → mirrors the source: the `uml:Model` wrapper (same name as the source's model) + - `false` → the `uml:Model` wrapper (named by `modelName`, else the source's model name) plus the filtered `xmi:Extension`. No wrapper is written if the source had none. - `true` → plain `uml:Package`, no model wrapper or extension. @@ -64,7 +66,7 @@ mockable. | --- | --- | | `Program.cs` | Entry point: parses args, deserializes the config, calls `Split`. | | `Configuration/SplitterConfig.cs`, `PackageConfig.cs` | Config models. | -| `Services/XmiSplitterService.cs` | Orchestrator: load → find packages (recursively, optionally scoped by `rootPackageName`) → stamp → map connectors → write. Mirrors the source's `uml:Model` wrapper rather than assuming one. Has a convenience ctor and an injectable ctor. | +| `Services/XmiSplitterService.cs` | Orchestrator: load → find packages (recursively, optionally scoped by `rootPackageName`) → stamp → map connectors → write. Names the `uml:Model` wrapper from `modelName`, else mirrors the source's. Has a convenience ctor and an injectable ctor. | | `Services/XmiModelLoader.cs` | UML4NET read, with the EA extender + extension content reader (inert for non-EA files). Also detects the source's UML namespace so output matches the input's UML version. | | `Services/PackageDocumentAssigner.cs` | Stamps `IXmiElement.DocumentName`. Traverses only `AggregationKind.Composite` properties (from UML4NET `[Property]` metadata) so *referenced* elements keep pointing at their own document. Returns contained + referenced ids. | | `Services/ExtensionBuilder.cs` | Filters the EA extension per package, deciding membership from the parsed EA `Element`/`Connector` and their resolved `ExtendedElement`. `CanFilter` is false for a foreign extension, which is then copied unchanged into every output. | diff --git a/README.md b/README.md index 0e11c6c..9da0f50 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ dotnet run --project XSplinter/XSplinter.csproj -- [-- - `rootPackageName` — *optional*; the root container package to scope the search to. Omit it when the packages sit directly under the model. +- `modelName` — *optional*; the name of the `uml:Model` wrapper written around each extracted + package. Defaults to the name of the model enclosing the package in the source document. - `packages[]` — the packages to extract, each with a `name`, an `outputFile` and an optional `convertToLibrary` flag. When `true`, the package is written as a plain `uml:Package` without the EA model wrapper or extension metadata; when `false` (default), the full EA model diff --git a/XSplinter.Tests/Services/XmiSplitterServiceTestFixture.cs b/XSplinter.Tests/Services/XmiSplitterServiceTestFixture.cs index 540af9f..d886d44 100644 --- a/XSplinter.Tests/Services/XmiSplitterServiceTestFixture.cs +++ b/XSplinter.Tests/Services/XmiSplitterServiceTestFixture.cs @@ -158,6 +158,36 @@ public void Verify_that_a_convertToLibrary_package_is_written_without_a_model_wr }); } + [Test] + public void Verify_that_the_model_wrapper_mirrors_the_source_when_no_name_is_configured() + { + this.xmiSplitterService.Split("input.xmi", this.config, "output"); + + var full = this.written.Single(document => document.Path.EndsWith("Forge.xmi")); + + Assert.That(full.Package.Name, Is.EqualTo("EA_Model")); + } + + [Test] + public void Verify_that_a_configured_model_name_overrides_the_source_wrapper() + { + this.config.ModelName = "Mycelium"; + + this.xmiSplitterService.Split("input.xmi", this.config, "output"); + + var full = this.written.Single(document => document.Path.EndsWith("Forge.xmi")); + var library = this.written.Single(document => document.Path.EndsWith("CSharp_Primitives.xmi")); + + Assert.Multiple(() => + { + Assert.That(full.Package.Name, Is.EqualTo("Mycelium")); + Assert.That(full.Package.PackagedElement.OfType().Single().Name, Is.EqualTo("Forge")); + + // a library package has no wrapper at all, so the setting does not apply + Assert.That(library.Package.Name, Is.EqualTo("Primitives")); + }); + } + [Test] public void Verify_that_the_xmi_documentation_header_is_only_written_for_full_EA_documents() { diff --git a/XSplinter/Configuration/PackageConfig.cs b/XSplinter/Configuration/PackageConfig.cs index 51e091f..86deff0 100644 --- a/XSplinter/Configuration/PackageConfig.cs +++ b/XSplinter/Configuration/PackageConfig.cs @@ -26,6 +26,18 @@ public class PackageConfig /// public string OutputFile { get; set; } = ""; + /// + /// Gets or sets the name of the top level element written to this package's output file. + /// + /// + /// Optional, and takes precedence over . For a normal + /// package this names the uml:Model wrapper; for a + /// package, which has no wrapper, it renames the uml:Package itself. When left empty + /// the wrapper keeps the name of the model enclosing the package in the source document and + /// a library package keeps its own name. + /// + public string ModelName { get; set; } = ""; + /// /// Gets or sets a value indicating whether this package should be converted into a /// simple reusable library. When false (the default), the package is written diff --git a/XSplinter/Configuration/SplitterConfig.cs b/XSplinter/Configuration/SplitterConfig.cs index f098509..c943a7e 100644 --- a/XSplinter/Configuration/SplitterConfig.cs +++ b/XSplinter/Configuration/SplitterConfig.cs @@ -27,6 +27,18 @@ public class SplitterConfig /// public string RootPackageName { get; set; } = ""; + /// + /// Gets or sets the name of the uml:Model wrapper written around each extracted + /// package (e.g. "EA_Model"). + /// + /// + /// Optional. When left empty the name of the model that encloses the package in the source + /// document is reused, so the output mirrors the input. The wrapper is not written at all + /// for packages marked , nor when the source + /// has no model. + /// + public string ModelName { get; set; } = ""; + /// /// Gets or sets the list of packages to extract into separate XMI files. /// diff --git a/XSplinter/Services/XmiSplitterService.cs b/XSplinter/Services/XmiSplitterService.cs index f508331..ee12a4c 100644 --- a/XSplinter/Services/XmiSplitterService.cs +++ b/XSplinter/Services/XmiSplitterService.cs @@ -196,15 +196,22 @@ public void Split(string inputPath, SplitterConfig config, string outputDirector if (packageConfig.ConvertToLibrary || enclosingModel == null) { - // no model wrapper: either explicitly requested, or the source did not have one + // no model wrapper: either explicitly requested, or the source did not have one. + // a name configured for this package therefore renames the package itself, which + // is the top level element of the document + if (!string.IsNullOrEmpty(packageConfig.ModelName)) + { + package.Name = packageConfig.ModelName; + } + this.documentWriter.Write(package, outputPath, null, null, loaded.UmlNamespaceUri); } else { - // mirror the wrapper of the source document rather than assuming one + // use the configured wrapper name, otherwise mirror the source document var model = new Model { - Name = enclosingModel.Name, + Name = QueryModelName(packageConfig, config, enclosingModel), DocumentName = packageConfig.OutputFile }; @@ -233,6 +240,33 @@ public void Split(string inputPath, SplitterConfig config, string outputDirector this.logger.LogInformation("Splitting complete"); } + /// + /// Determines the name of the uml:Model wrapper: the name configured for the package, + /// otherwise the name configured for the whole run, otherwise the name of the model that + /// encloses the package in the source document. + /// + /// + /// The configuration of the package being written. + /// + /// + /// The splitter configuration. + /// + /// + /// The model enclosing the package in the source document. + /// + /// + /// The name to write on the model wrapper. + /// + private static string QueryModelName(PackageConfig packageConfig, SplitterConfig config, IModel enclosingModel) + { + if (!string.IsNullOrEmpty(packageConfig.ModelName)) + { + return packageConfig.ModelName; + } + + return string.IsNullOrEmpty(config.ModelName) ? enclosingModel.Name : config.ModelName; + } + /// /// Walks the package hierarchy of the document and records every package together with the /// that encloses it, so that the split documents can mirror the diff --git a/XSplinter/XSplinter.csproj b/XSplinter/XSplinter.csproj index 2634680..fc370d2 100644 --- a/XSplinter/XSplinter.csproj +++ b/XSplinter/XSplinter.csproj @@ -17,7 +17,7 @@ Starion Group S.A. Copyright © 2026 Starion Group S.A. Apache-2.0 - EA XMI UML EnterpriseArchitect splitter + monolithic XMI UML splitter https://github.com/STARIONGROUP/X-Splinter.git Git en-US