Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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. |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ dotnet run --project XSplinter/XSplinter.csproj -- <input.xmi> <config.json> [--

- `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
Expand Down
30 changes: 30 additions & 0 deletions XSplinter.Tests/Services/XmiSplitterServiceTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPackage>().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()
{
Expand Down
12 changes: 12 additions & 0 deletions XSplinter/Configuration/PackageConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ public class PackageConfig
/// </summary>
public string OutputFile { get; set; } = "";

/// <summary>
/// Gets or sets the name of the top level element written to this package's output file.
/// </summary>
/// <remarks>
/// Optional, and takes precedence over <see cref="SplitterConfig.ModelName"/>. For a normal
/// package this names the <c>uml:Model</c> wrapper; for a <see cref="ConvertToLibrary"/>
/// package, which has no wrapper, it renames the <c>uml:Package</c> 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.
/// </remarks>
public string ModelName { get; set; } = "";

/// <summary>
/// Gets or sets a value indicating whether this package should be converted into a
/// simple reusable library. When <c>false</c> (the default), the package is written
Expand Down
12 changes: 12 additions & 0 deletions XSplinter/Configuration/SplitterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ public class SplitterConfig
/// </remarks>
public string RootPackageName { get; set; } = "";

/// <summary>
/// Gets or sets the name of the <c>uml:Model</c> wrapper written around each extracted
/// package (e.g. "EA_Model").
/// </summary>
/// <remarks>
/// 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 <see cref="PackageConfig.ConvertToLibrary"/>, nor when the source
/// has no model.
/// </remarks>
public string ModelName { get; set; } = "";

/// <summary>
/// Gets or sets the list of packages to extract into separate XMI files.
/// </summary>
Expand Down
40 changes: 37 additions & 3 deletions XSplinter/Services/XmiSplitterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
/// <exception cref="InvalidOperationException">
/// Thrown when the root package or a configured child package is not found in the XMI.
/// </exception>
public void Split(string inputPath, SplitterConfig config, string outputDirectory)

Check warning on line 121 in XSplinter/Services/XmiSplitterService.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

Check warning on line 121 in XSplinter/Services/XmiSplitterService.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

Check warning on line 121 in XSplinter/Services/XmiSplitterService.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

Check warning on line 121 in XSplinter/Services/XmiSplitterService.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.
{
this.documentWriter.EnsureDirectory(outputDirectory);

Expand Down Expand Up @@ -196,15 +196,22 @@

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
};

Expand Down Expand Up @@ -233,6 +240,33 @@
this.logger.LogInformation("Splitting complete");
}

/// <summary>
/// Determines the name of the <c>uml:Model</c> 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.
/// </summary>
/// <param name="packageConfig">
/// The configuration of the package being written.
/// </param>
/// <param name="config">
/// The splitter configuration.
/// </param>
/// <param name="enclosingModel">
/// The model enclosing the package in the source document.
/// </param>
/// <returns>
/// The name to write on the model wrapper.
/// </returns>
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;
}

/// <summary>
/// Walks the package hierarchy of the document and records every package together with the
/// <see cref="IModel"/> that encloses it, so that the split documents can mirror the
Expand Down
2 changes: 1 addition & 1 deletion XSplinter/XSplinter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<Company>Starion Group S.A.</Company>
<Copyright>Copyright © 2026 Starion Group S.A.</Copyright>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>EA XMI UML EnterpriseArchitect splitter</PackageTags>
<PackageTags>monolithic XMI UML splitter</PackageTags>
<RepositoryUrl>https://github.com/STARIONGROUP/X-Splinter.git</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<NeutralLanguage>en-US</NeutralLanguage>
Expand Down
Loading