Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public static ScaffoldResult Apply(ComponentScaffoldRequest request)
throw new NotSupportedException(
$"Component type '{request.ComponentType}' has no scaffold applier. Supported types: {string.Join(", ", Appliers.Keys)}.");
}
// The wire value is only a hint: templates and the CLI no longer have to pass a
// real path - anything that is not an actual solution root is ignored and the
// root is auto-detected from the current directory.
request.SolutionRootPath = SolutionRootLocator.Resolve(request.SolutionRootPath, Directory.GetCurrentDirectory());
return applier(request);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Xml;

namespace TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding;

/// <summary>
/// Determines the unpacked solution root for a scaffold request, so callers never
/// have to pass SolutionRootPath: a value that does not point at an existing
/// directory (an unrendered template placeholder) goes into the void and the root
/// is detected from the base directory instead - Other/Solution.xml in the base
/// itself, the SolutionRootPath property of the single .csproj, or a unique
/// Other/Solution.xml found below the base (same rules as the template-side detector).
/// </summary>
public static class SolutionRootLocator
{
public static string Resolve(string? requested, string baseDirectory)
{
if (!string.IsNullOrWhiteSpace(requested))
{
var requestedPath = Path.IsPathRooted(requested) ? requested : Path.Combine(baseDirectory, requested);
if (Directory.Exists(requestedPath)) return requested;
}

if (IsSolutionRoot(baseDirectory)) return baseDirectory;

var fromProject = FromProjectProperty(baseDirectory);
if (fromProject != null) return Path.Combine(baseDirectory, fromProject);

return FromDirectorySearch(baseDirectory);
}

private static bool IsSolutionRoot(string directory) =>
File.Exists(Path.Combine(directory, "Other", "Solution.xml"));

private static string? FromProjectProperty(string baseDirectory)
{
var projects = Directory.Exists(baseDirectory)
? Directory.GetFiles(baseDirectory, "*.csproj", SearchOption.TopDirectoryOnly)
: Array.Empty<string>();
if (projects.Length > 1)
{
throw new InvalidOperationException(
$"Multiple .csproj files found in '{baseDirectory}': {string.Join(", ", projects.Select(Path.GetFileName))}");
}
if (projects.Length == 0) return null;

var doc = new XmlDocument();
try
{
doc.Load(projects[0]);
}
catch (XmlException)
{
return null;
}

var node = doc.SelectSingleNode(
"/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='SolutionRootPath']");
var value = node?.InnerText.Trim();
return string.IsNullOrWhiteSpace(value) ? null : value;
}

private static string FromDirectorySearch(string baseDirectory)
{
var candidates = Directory.Exists(baseDirectory)
? Directory.GetFiles(baseDirectory, "Solution.xml", SearchOption.AllDirectories)
.Where(path => string.Equals(Path.GetFileName(Path.GetDirectoryName(path)), "Other", StringComparison.OrdinalIgnoreCase))
.ToArray()
: Array.Empty<string>();

if (candidates.Length > 1)
{
throw new InvalidOperationException(
$"Multiple Other/Solution.xml files found under '{baseDirectory}':{Environment.NewLine}{string.Join(Environment.NewLine, candidates)}");
}
if (candidates.Length == 0)
throw new InvalidOperationException($"Failed to determine SolutionRootPath from '{baseDirectory}'.");

return Path.GetDirectoryName(Path.GetDirectoryName(candidates[0]))!;
}
}
76 changes: 76 additions & 0 deletions tests/TALXIS.Platform.Metadata.Tests/SolutionRootLocatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using TALXIS.Platform.Metadata.Serialization.Xml.Scaffolding;

namespace TALXIS.Platform.Metadata.Tests;

public class SolutionRootLocatorTests : IDisposable
{
private readonly string _base = Directory.CreateTempSubdirectory("metadata-solution-root-locator").FullName;

public void Dispose() => Directory.Delete(_base, recursive: true);

private void CreateSolutionRoot(params string[] segments)
{
var other = Path.Combine(Path.Combine(new[] { _base }.Concat(segments).ToArray()), "Other");
Directory.CreateDirectory(other);
File.WriteAllText(Path.Combine(other, "Solution.xml"), "<ImportExportXml />");
}

[Fact]
public void Resolve_ValidRequestedRoot_IsHonored()
{
CreateSolutionRoot("Declarations");

var resolved = SolutionRootLocator.Resolve("Declarations", _base);

Assert.Equal("Declarations", resolved);
}

[Fact]
public void Resolve_PlaceholderRequested_DetectsBaseDirectoryRoot()
{
CreateSolutionRoot();

var resolved = SolutionRootLocator.Resolve("__solution-root-path__", _base);

Assert.Equal(_base, resolved);
}

[Fact]
public void Resolve_ReadsCsprojProperty()
{
CreateSolutionRoot("Declarations", "Source");
Directory.CreateDirectory(Path.Combine(_base, "Declarations", "Decoy"));
File.WriteAllText(Path.Combine(_base, "Sandbox.csproj"), """
<Project Sdk="TALXIS.DevKit.Sdk.Dataverse"><PropertyGroup><SolutionRootPath>Declarations/Source</SolutionRootPath></PropertyGroup></Project>
""");

var resolved = SolutionRootLocator.Resolve(null, _base);

Assert.Equal(Path.Combine(_base, "Declarations/Source"), resolved);
}

[Fact]
public void Resolve_FallsBackToUniqueNestedRoot()
{
CreateSolutionRoot("Declarations");

var resolved = SolutionRootLocator.Resolve(null, _base);

Assert.Equal(Path.Combine(_base, "Declarations"), resolved);
}

[Fact]
public void Resolve_MultipleNestedRoots_Throws()
{
CreateSolutionRoot("A");
CreateSolutionRoot("B");

Assert.Throws<InvalidOperationException>(() => SolutionRootLocator.Resolve(null, _base));
}

[Fact]
public void Resolve_NothingFound_Throws()
{
Assert.Throws<InvalidOperationException>(() => SolutionRootLocator.Resolve(null, _base));
}
}