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
65 changes: 65 additions & 0 deletions Schema.Test/SchemaDataSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,71 @@ public void TestCodeGeneratorOutputPathResolves()
Assert.AreEqual(Path.GetFullPath(Path.Combine(workingDirectory, "generated")), resolved.ToString());
}

/// <summary>
/// A resolved path is normalised, not merely concatenated. A schema beside the data it
/// describes is the easy case; one that reaches a sibling directory through <c>..</c> is the
/// case that tells a resolver from a string join, and the resolved value is compared,
/// displayed and used as a key, so it has to come back as the directory it names rather than
/// as a route to it.
/// </summary>
[TestMethod]
public void TestFilePathsResolveThroughTraversalSegments()
{
Schema schema = CreateAnchoredSchema("../shared/./items.json");

Assert.IsTrue(schema.GetDataSource("Items".As<DataSourceName>())!.TryResolveFile(out AbsoluteFilePath resolved));
Assert.AreEqual(
Path.GetFullPath(Path.Combine(workingDirectory, "../shared/./items.json")),
resolved.ToString());
Assert.IsFalse(resolved.ToString().Contains("..", StringComparison.Ordinal), resolved.ToString());
}

[TestMethod]
public void TestDirectoryPathsResolveThroughTraversalSegments()
{
Schema schema = CreateAnchoredSchema();
SchemaCodeGenerator generator = schema.AddCodeGenerator("CSharp".As<CodeGeneratorName>())!;
generator.OutputPath = "../build/./generated".As<RelativeDirectoryPath>();

Assert.IsTrue(generator.TryResolveOutputPath(out AbsoluteDirectoryPath resolved));
Assert.AreEqual(
Path.GetFullPath(Path.Combine(workingDirectory, "../build/./generated")),
resolved.ToString());
Assert.IsFalse(resolved.ToString().Contains("..", StringComparison.Ordinal), resolved.ToString());
}

/// <summary>
/// The anchor is the directory holding the schema file, and nothing else about the file.
/// </summary>
[TestMethod]
public void TestTheAnchorIsTheSchemaFilesDirectory()
{
Schema schema = new();
schema.SetSourceFile(SchemaPath);

Assert.AreEqual(workingDirectory, schema.SourceDirectory.ToString());
Assert.AreEqual("test.schema.json", schema.SourceFileName);
}

/// <summary>
/// Anchoring a schema must not disturb the path it was handed. Callers pass an instance they
/// keep using - the editor records the same one as a recent file straight afterwards - and
/// reading <c>AbsoluteFilePath.AbsoluteDirectoryPath</c> to find the anchor silently breaks
/// equality and the hash code of the instance it is read from (ktsu.Semantics.Paths 5.4.2),
/// while leaving its text alone. That is why the anchor is still taken from the string.
/// </summary>
[TestMethod]
public void TestSettingTheSourceFileLeavesTheCallersPathEqualToItself()
{
AbsoluteFilePath handedIn = SchemaPath;
AbsoluteFilePath untouched = SchemaPath;

new Schema().SetSourceFile(handedIn);

Assert.AreEqual(untouched, handedIn, "Anchoring the schema changed the path it was given.");
Assert.AreEqual(untouched.GetHashCode(), handedIn.GetHashCode(), "Anchoring the schema changed the hash code of the path it was given.");
}

[TestMethod]
public void TestLoadWithASourcePathAnchorsTheSchema()
{
Expand Down
17 changes: 15 additions & 2 deletions Schema/Models/Schema.Paths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ namespace ktsu.Schema.Models;
/// The anchor is supplied by whoever read the file, so the serializer itself stays free of the
/// filesystem. A schema that was never read from a file has no anchor and cannot resolve
/// anything, which every resolution API reports rather than guessing at the working directory.
///
/// Resolution goes through <c>AsAbsolute</c> rather than the <c>/</c> combine operator: a schema
/// may reach a sibling directory through <c>..</c>, and only the former normalises those segments
/// away. The operator joins, which hands back a route to the file rather than the file.
///
/// The anchor is still taken with <see cref="Path.GetDirectoryName(string)"/> rather than
/// <c>AbsoluteFilePath.AbsoluteDirectoryPath</c>, which would read better. Reading that property
/// mutates the instance it is read from: in <c>ktsu.Semantics.Paths</c> 5.4.2 an
/// <c>AbsoluteFilePath</c> stops comparing equal to an identical one, and its hash code changes,
/// once the property has been touched, while its text stays the same. Callers hand the same
/// instance on afterwards - the editor records it as a recent file - so reading it here corrupted
/// equality for a value this code does not own. <c>AsAbsolute</c> carries no such hazard, which is
/// why only the resolution moved.
/// </remarks>
public partial class Schema
{
Expand All @@ -40,7 +53,7 @@ public bool TryResolvePath(RelativeFilePath relativePath, out AbsoluteFilePath r
return false;
}

resolved = Path.GetFullPath(Path.Combine(SourceDirectory, relativePath)).As<AbsoluteFilePath>();
resolved = relativePath.AsAbsolute(SourceDirectory);
return true;
}

Expand All @@ -59,7 +72,7 @@ public bool TryResolvePath(RelativeDirectoryPath relativePath, out AbsoluteDirec
return false;
}

resolved = Path.GetFullPath(Path.Combine(SourceDirectory, relativePath)).As<AbsoluteDirectoryPath>();
resolved = relativePath.AsAbsolute(SourceDirectory);
return true;
}

Expand Down
Loading