Skip to content

Commit

Permalink
feat: stable version resolving logic
Browse files Browse the repository at this point in the history
This adds the domain logic for resolving the latest stable packument version from a packument. Not used by the cli yet
  • Loading branch information
ComradeVanti committed Sep 29, 2024
1 parent 474464a commit 7785813
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/cli/error-logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "../domain/common-errors";
import { stringifyEditorVersion } from "../domain/editor-version";
import {
NoStableError,
NoVersionsError,
VersionNotFoundError,
type ResolvePackumentVersionError,
Expand Down Expand Up @@ -96,6 +97,8 @@ function makeErrorMessageFor(error: unknown): string {
return "Could not determine path of home directory.";
if (error instanceof NoSystemUserProfilePath)
return "Could not determine path of system user directory.";
if (error instanceof NoStableError)
return "Seems like the package you requested has no stable versions.";
return "A fatal error occurred.";
}

Expand Down
16 changes: 13 additions & 3 deletions src/domain/package-reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import { SemanticVersion } from "./semantic-version";
import { trySplitAtFirstOccurrenceOf } from "./string-utils";
import { assertZod, isZod } from "./zod-utils";

/**
* The "latest" tag string. Specifies that the latest version is requested.
*/
export type LatestTag = "latest";

/**
* The "stable" tag string. Specifies that the latest stable version is
* requested.
*/
export type StableTag = "stable";

/**
* A string with the format of one of the supported version tags.
* NOTE: Currently we only support "latest".
*/
export type PackageTag = "latest";
export type PackageTag = LatestTag | StableTag;

/**
* Reference to a version, either directly by a semantic version or via an
Expand All @@ -23,7 +33,7 @@ export type VersionReference = SemanticVersion | PackageUrl | PackageTag;
export type ReferenceWithVersion = `${DomainName}@${VersionReference}`;

/**
* A version-reference that is resolvable.
* A {@link VersionReference} that is resolvable.
* Mostly this excludes {@link PackageUrl}s.
*/
export type ResolvableVersion = Exclude<VersionReference, PackageUrl>;
Expand Down
58 changes: 46 additions & 12 deletions src/domain/packument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { Err, Ok, Result } from "ts-results-es";
import { PackumentNotFoundError } from "./common-errors";
import { DomainName } from "./domain-name";
import { UnityPackageManifest } from "./package-manifest";
import { ResolvableVersion } from "./package-reference";
import {
ResolvableVersion,
type LatestTag,
type StableTag,
} from "./package-reference";
import { recordKeys } from "./record-utils";
import { SemanticVersion } from "./semantic-version";
import { compareVersions, isStable, SemanticVersion } from "./semantic-version";

/**
* Contains information about a specific version of a package. This is based on
Expand Down Expand Up @@ -140,6 +144,21 @@ export class VersionNotFoundError extends CustomError {
}
}

/**
* Error for when the latest stable version of a packument was requested, but
* the packument had no stable versions.
*/
export class NoStableError extends CustomError {
constructor(
/**
* The name of the packument.
*/
public readonly packageName: DomainName
) {
super();
}
}

/**
* A failed attempt at resolving a packument-version.
*/
Expand All @@ -157,8 +176,20 @@ export type ResolvePackumentVersionError =
*/
export function tryResolvePackumentVersion(
packument: UnityPackument,
requestedVersion: "latest"
requestedVersion: LatestTag
): Result<UnityPackumentVersion, never>;
/**
* Resolved the latest stable version from a packument.
* @param packument The packument.
* @param requestedVersion The version to resolve. In this case indicates that
* the latest stable version is requested.
* @returns Result containing the resolved version or an error.
* @throws {NoVersionsError} If the packument had no versions at all.
*/
export function tryResolvePackumentVersion(
packument: UnityPackument,
requestedVersion: StableTag
): Result<UnityPackumentVersion, NoStableError>;
/**
* Attempts to resolve a specific version from a packument.
* @param packument The packument.
Expand Down Expand Up @@ -187,24 +218,27 @@ export function tryResolvePackumentVersion(
packument: UnityPackument,
requestedVersion: ResolvableVersion
) {
const availableVersions = recordKeys(packument.versions);
if (availableVersions.length === 0) throw new NoVersionsError(packument.name);
const allVersions = recordKeys(packument.versions);
if (allVersions.length === 0) throw new NoVersionsError(packument.name);
const sortedVersions = allVersions.slice().sort(compareVersions);

// Find the latest version
if (requestedVersion === "latest") {
let latestVersion = tryGetLatestVersion(packument);
if (latestVersion === null) latestVersion = availableVersions.at(-1)!;
if (latestVersion === null) latestVersion = sortedVersions.at(-1)!;
return Ok(tryGetPackumentVersion(packument, latestVersion)!);
}

if (requestedVersion === "stable") {
const version = sortedVersions.findLast(isStable);
if (version === undefined) return Err(new NoStableError(packument.name));
return Ok(tryGetPackumentVersion(packument, version)!);
}

// Find a specific version
if (!availableVersions.includes(requestedVersion))
if (!sortedVersions.includes(requestedVersion))
return Err(
new VersionNotFoundError(
packument.name,
requestedVersion,
availableVersions
)
new VersionNotFoundError(packument.name, requestedVersion, sortedVersions)
);

return Ok(tryGetPackumentVersion(packument, requestedVersion)!);
Expand Down
25 changes: 25 additions & 0 deletions src/domain/semantic-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,28 @@ export const SemanticVersion = z
* @see https://semver.org/.
*/
export type SemanticVersion = z.TypeOf<typeof SemanticVersion>;

/**
* Compares to semantic versions to see which is larger. Can be used for
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted Array.prototype.toSorted}.
* @param a The first version.
* @param b The second version.
* @returns A number indicating the sorting order of the numbers.
* - a > b -> 1.
* - a = b -> 0.
* - a < b -> -1.
*/
export function compareVersions(
a: SemanticVersion,
b: SemanticVersion
): -1 | 0 | 1 {
return semver.compare(a, b, false);
}

/**
* Checks wheter a semantic version is stable.
* @param version The version to check.
*/
export function isStable(version: SemanticVersion): boolean {
return semver.prerelease(version) === null;
}
6 changes: 5 additions & 1 deletion test/common/data-packument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ class UnityPackumentBuilder {

/**
* Adds a version to this package.
* The order in which you add versions to the packument is important because
* every time you call this function the version specified in the packument's
* "dist-tags" is overriden to the given version.
* @param version The name of the version.
* @param build A builder function.
* @returns The builder for chaining.
*/
addVersion(
version: string,
build?: (builder: UnityPackumentVersionBuilder) => unknown
): UnityPackumentBuilder {
): this {
assert(isZod(version, SemanticVersion), `${version} is semantic version`);
const builder = new UnityPackumentVersionBuilder(
this.packument.name,
Expand Down
50 changes: 47 additions & 3 deletions test/unit/domain/packument.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Ok } from "ts-results-es";
import { DomainName } from "../../../src/domain/domain-name";
import {
NoStableError,
NoVersionsError,
packumentHasVersion,
tryGetLatestVersion,
Expand Down Expand Up @@ -87,10 +88,30 @@ describe("packument", () => {
).toThrow(NoVersionsError);
});

it("should find latest version when requested", () => {
const result = tryResolvePackumentVersion(somePackument, "latest");
it("should find latest version for packument where latest is stable", () => {
const packument = buildPackument(somePackage, (packument) =>
packument.addVersion("0.8.0").addVersion("1.0.0")
);

const packumentVersion = tryResolvePackumentVersion(
packument,
"latest"
).unwrap();

expect(packumentVersion.version).toEqual("1.0.0");
});

expect(result).toEqual(Ok(somePackument.versions[someHighVersion]!));
it("should find latest version for packument where latest is pre", () => {
const packument = buildPackument(somePackage, (packument) =>
packument.addVersion("1.0.0").addVersion("1.1.0-pre")
);

const packumentVersion = tryResolvePackumentVersion(
packument,
"latest"
).unwrap();

expect(packumentVersion.version).toEqual("1.1.0-pre");
});

it("should find specific version", () => {
Expand All @@ -110,6 +131,29 @@ describe("packument", () => {
const error = result.unwrapErr();
expect(error).toBeInstanceOf(VersionNotFoundError);
});

it("should find latest stable version if there is one", () => {
const packument = buildPackument(somePackage, (packument) =>
packument.addVersion("2.0.0-pre").addVersion("1.0.0")
);

const packumentVersion = tryResolvePackumentVersion(
packument,
"stable"
).unwrap();

expect(packumentVersion.version).toEqual("1.0.0");
});

it("should fail to find latest stable version if there is none", () => {
const packument = buildPackument(somePackage, (packument) =>
packument.addVersion("1.0.0-pre.0").addVersion("1.0.0-pre.1")
);

const error = tryResolvePackumentVersion(packument, "stable").unwrapErr();

expect(error).toBeInstanceOf(NoStableError);
});
});

describe("has version", () => {
Expand Down

0 comments on commit 7785813

Please sign in to comment.