Document public module APIs (#1813)

This commit is contained in:
Maycon Campos authored and GitHub committed 2026-10-15 13:27:05 -07:00
1 parent 6811abf11c
commit 8399f11186
7 files changed
+104 -2

No files matched your search

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,13 +26,29 @@ import java.util.Collections;
import java.util.List;
import org.pkl.core.util.IoUtils;
/** Utilities for inspecting file-system paths while resolving module glob imports. */
public final class FileResolver {
private FileResolver() {}
/**
* Returns the immediate, non-symbolic-link children of the directory identified by {@code
* baseUri}.
*
* <p>Returns an empty list if {@code baseUri} does not exist or does not identify a directory.
*
* @throws IOException if the directory cannot be read
*/
public static List<PathElement> listElements(URI baseUri) throws IOException {
return listElements(IoUtils.pathOf(baseUri));
}
/**
* Returns the immediate, non-symbolic-link children of {@code path}.
*
* <p>Returns an empty list if {@code path} does not exist or is not a directory.
*
* @throws IOException if the directory cannot be read
*/
public static List<PathElement> listElements(Path path) throws IOException {
try (var stream = Files.newDirectoryStream(path)) {
var ret = new ArrayList<PathElement>();
@@ -49,10 +65,12 @@ public final class FileResolver {
}
}
/** Returns whether the file-system path identified by {@code elementUri} exists. */
public static boolean hasElement(URI elementUri) {
return Files.exists(IoUtils.pathOf(elementUri));
}
/** Returns whether {@code path} exists. */
public static boolean hasElement(Path path) {
return Files.exists(path);
}
@@ -57,10 +57,19 @@ public final class ModulePathResolver implements AutoCloseable {
private static final ModulePathResolver EMPTY = new ModulePathResolver(Collections.emptyList());
/** Returns a resolver whose module path contains no entries. */
public static ModulePathResolver empty() {
return EMPTY;
}
/**
* Creates a resolver for the given module path entries.
*
* <p>Each entry can be a directory, JAR file, or ZIP file. Entries are searched in iteration
* order; if more than one entry contains the same path, the first entry wins.
*
* @param modulePath the module path entries to search
*/
public ModulePathResolver(Iterable<Path> modulePath) {
this.modulePath = modulePath;
}
@@ -103,6 +112,13 @@ public final class ModulePathResolver implements AutoCloseable {
}
}
/**
* Resolves a {@code modulepath:} URI to the path containing its source.
*
* @throws FileNotFoundException if the module path contains no matching file
* @throws IOException if a module path entry cannot be read
* @throws IllegalStateException if this resolver has been closed
*/
public Path resolve(URI uri) throws IOException {
var modulePath = getModulePath(uri);
var result = getFileCache().get(modulePath);
@@ -111,6 +127,12 @@ public final class ModulePathResolver implements AutoCloseable {
throw new FileNotFoundException();
}
/**
* Returns whether the module path contains the element identified by {@code elementUri}.
*
* @throws UncheckedIOException if a module path entry cannot be read
* @throws IllegalStateException if this resolver has been closed
*/
public boolean hasElement(URI elementUri) {
var path = elementUri.getPath();
try {
@@ -121,6 +143,11 @@ public final class ModulePathResolver implements AutoCloseable {
}
}
/**
* Closes file systems opened for JAR and ZIP entries.
*
* <p>Calling this method more than once has no effect.
*/
@Override
public void close() {
synchronized (lock) {
@@ -24,7 +24,9 @@ import org.graalvm.collections.EconomicMap;
import org.jspecify.annotations.Nullable;
import org.pkl.core.util.EconomicMaps;
/** Describes a child of a hierarchical module or resource path. */
public class PathElement {
/** Orders files before directories, then orders elements lexicographically by name. */
public static final Comparator<PathElement> comparator =
(o1, o2) -> {
if (o1.isDirectory && !o2.isDirectory) {
@@ -39,19 +41,23 @@ public class PathElement {
private final boolean isDirectory;
/** Creates an element whose type cannot be inspected and is therefore treated as a file. */
public static PathElement opaque(String name) {
return new PathElement(name, false);
}
/** Creates an element with the given name and directory status. */
public PathElement(String name, boolean isDirectory) {
this.name = name;
this.isDirectory = isDirectory;
}
/** Returns this element's name relative to its parent. */
public String getName() {
return name;
}
/** Returns an equivalent element with {@code name}, or this element if its name is unchanged. */
public PathElement withName(String name) {
if (name.equals(this.name)) {
return this;
@@ -59,6 +65,7 @@ public class PathElement {
return new PathElement(name, isDirectory);
}
/** Returns whether this element represents a directory. */
public boolean isDirectory() {
return isDirectory;
}
@@ -80,13 +87,19 @@ public class PathElement {
return "PathElement{" + "name='" + name + '\'' + ", isDirectory=" + isDirectory + '}';
}
/** A path element that stores its descendants as a tree. */
public static final class TreePathElement extends PathElement {
private final EconomicMap<String, TreePathElement> children = EconomicMaps.create();
/** Creates a tree element with the given name and directory status. */
public TreePathElement(String name, boolean isDirectory) {
super(name, isDirectory);
}
/**
* Adds {@code child} unless a child named {@code name} already exists, and returns the stored
* child.
*/
public TreePathElement putIfAbsent(String name, TreePathElement child) {
children.putIfAbsent(name, child);
return children.get(name);
@@ -113,10 +126,12 @@ public class PathElement {
return getElement(Path.of(basePath));
}
/** Returns this element's children, keyed by name. */
public EconomicMap<String, TreePathElement> getChildren() {
return children;
}
/** Returns a snapshot of this element's child values. */
public List<PathElement> getChildrenValues() {
var ret = new ArrayList<PathElement>(children.size());
for (var elem : EconomicMaps.getValues(children)) {
@@ -40,9 +40,12 @@ import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.json.Json.JsonParseException;
/** Resolves a project's declared package dependencies against its lock file. */
public final class ProjectDependenciesManager {
/** The conventional name of a Pkl project file. */
public static final String PKL_PROJECT_FILENAME = "PklProject";
/** The conventional name of a Pkl project's dependency lock file. */
public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json";
private final DeclaredDependencies declaredDependencies;
@@ -66,6 +69,13 @@ public final class ProjectDependenciesManager {
private final Object lock = new Object();
/**
* Creates a dependency manager for a project.
*
* @param declaredDependencies the dependencies declared by the project
* @param moduleResolver the resolver used to load the project's dependency lock file
* @param securityManager the security manager used when loading the dependency lock file
*/
public ProjectDependenciesManager(
DeclaredDependencies declaredDependencies,
ModuleResolver moduleResolver,
@@ -77,6 +87,7 @@ public final class ProjectDependenciesManager {
this.securityManager = securityManager;
}
/** Returns whether {@code uri} is located within this project. */
public boolean hasUri(URI uri) {
return projectBaseUri.getScheme().equals(uri.getScheme())
&& Objects.equals(projectBaseUri.getAuthority(), uri.getAuthority())
@@ -164,6 +175,7 @@ public final class ProjectDependenciesManager {
}
// `ensureDependenciesInitialized` makes `myDependencies` safe to access
/** Returns the project's direct dependencies, keyed by their declared names. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public Map<String, Dependency> getDependencies() {
ensureDependenciesInitialized();
@@ -172,6 +184,7 @@ public final class ProjectDependenciesManager {
}
// `ensureDependenciesInitialized` makes `localPackageDependencies` safe to access
/** Returns whether {@code packageUri} identifies a local dependency of this project. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public boolean isLocalPackage(PackageUri packageUri) {
ensureDependenciesInitialized();
@@ -179,6 +192,7 @@ public final class ProjectDependenciesManager {
}
// `ensureDependenciesInitialized` makes `localPackageDependencies` safe to access
/** Returns the dependencies of the local package identified by {@code packageUri}. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public Map<String, Dependency> getLocalPackageDependencies(PackageUri packageUri) {
ensureDependenciesInitialized();
@@ -188,6 +202,13 @@ public final class ProjectDependenciesManager {
return dep;
}
/**
* Resolves a package's declared dependencies against this project's lock file.
*
* @param packageUri the package whose dependencies are being resolved
* @param dependencyMetadata the package's published dependency metadata
* @return dependencies keyed by their names in the package metadata
*/
public Map<String, Dependency> getResolvedDependenciesForPackage(
PackageUri packageUri, DependencyMetadata dependencyMetadata) {
synchronized (lock) {
@@ -217,10 +238,12 @@ public final class ProjectDependenciesManager {
}
}
/** Returns this project's declared dependencies. */
public DeclaredDependencies getDeclaredDependencies() {
return declaredDependencies;
}
/** Returns the locked dependency matching {@code packageUri}. */
public Dependency getResolvedDependency(PackageUri packageUri) {
var dep = getProjectDeps().get(CanonicalPackageUri.fromPackageUri(packageUri));
if (dep == null) {
@@ -229,14 +252,17 @@ public final class ProjectDependenciesManager {
return dep;
}
/** Returns the base URI used to resolve paths within this project. */
public URI getProjectBaseUri() {
return projectBaseUri;
}
/** Returns the URI of this project's dependency lock file. */
public URI getProjectDepsFileUri() {
return IoUtils.resolve(projectBaseUri, PKL_PROJECT_DEPS_FILENAME);
}
/** Returns the URI of this project's project file. */
public URI getProjectFileUri() {
return declaredDependencies.projectFileUri();
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import org.pkl.core.SecurityManager;
/** SPI for identifying a resolved module and loading its source code. */
public interface ResolvedModuleKey {
/** Returns the unresolved module key from which this key was resolved. */
ModuleKey getOriginal();
/**
@@ -34,6 +34,7 @@ public final class ResolvedModuleKeys {
* loaded from that file path and cached using the given URI as cache key.
*
* @param nofollow if true, the file will be opened with {@link LinkOption#NOFOLLOW_LINKS}.
* @return a resolved module key backed by {@code path}
*/
public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path, boolean nofollow) {
return new FileKey(original, uri, path, nofollow);
@@ -42,6 +43,8 @@ public final class ResolvedModuleKeys {
/**
* Creates a resolved module key backed by the given file path. The resulting module will be
* loaded from that file path and cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code path}
*/
public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path) {
return new FileKey(original, uri, path, false);
@@ -50,6 +53,8 @@ public final class ResolvedModuleKeys {
/**
* Creates a resolved module key backed by the given URL. The resulting module will be loaded from
* that URL and cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code url}
*/
public static ResolvedModuleKey url(ModuleKey original, URI uri, URL url) {
return new Url(original, uri, url);
@@ -58,6 +63,8 @@ public final class ResolvedModuleKeys {
/**
* Creates a resolved module key backed by the given source code. If {@code cached} is {@code
* true}, the resulting module will be cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code sourceText}
*/
public static ResolvedModuleKey virtual(
ModuleKey original, URI uri, String sourceText, boolean cached) {
@@ -67,6 +74,8 @@ public final class ResolvedModuleKeys {
/**
* Creates a resolved module key that behaves like {@code delegate}, except with {@code original}
* as its original module key.
*
* @return a resolved module key backed by {@code delegate}
*/
public static ResolvedModuleKey delegated(ResolvedModuleKey delegate, ModuleKey original) {
return new Delegated(delegate, original);
@@ -1,3 +1,9 @@
/**
* APIs for resolving Pkl module URIs and loading module source code.
*
* <p>Custom module schemes can be implemented with {@link org.pkl.core.module.ModuleKeyFactory},
* {@link org.pkl.core.module.ModuleKey}, and {@link org.pkl.core.module.ResolvedModuleKey}.
*/
@NullMarked
package org.pkl.core.module;