diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt index cdcd4e33..0e87e7f8 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliProjectPackagerTest.kt @@ -980,6 +980,92 @@ class CliProjectPackagerTest { ) } + @Test + fun `generate annotations`(@TempDir tempDir: Path) { + tempDir + .resolve("PklProject") + .writeString( + """ + @Unlisted + @Deprecated { since = "0.26.1"; message = "do not use" } + @ModuleInfo { minPklVersion = "0.26.0" } + amends "pkl:Project" + + package { + name = "mypackage" + version = "1.0.0" + baseUri = "package://example.com/mypackage" + packageZipUrl = "https://foo.com" + } + """ + .trimIndent() + ) + val packager = + CliProjectPackager( + CliBaseOptions(workingDir = tempDir), + listOf(tempDir), + CliTestOptions(), + ".out/%{name}@%{version}", + skipPublishCheck = true, + consoleWriter = StringWriter() + ) + packager.run() + val expectedMetadata = tempDir.resolve(".out/mypackage@1.0.0/mypackage@1.0.0") + assertThat(expectedMetadata).exists() + assertThat(expectedMetadata) + .hasContent( + """ + { + "name": "mypackage", + "packageUri": "package://example.com/mypackage@1.0.0", + "version": "1.0.0", + "packageZipUrl": "https://foo.com", + "packageZipChecksums": { + "sha256": "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85" + }, + "dependencies": {}, + "authors": [], + "annotations": [ + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Unlisted", + "moduleUri": "pkl:base" + }, + "properties": {} + }, + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Deprecated", + "moduleUri": "pkl:base" + }, + "properties": { + "since": "0.26.1", + "message": "do not use", + "replaceWith": null + } + }, + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "ModuleInfo", + "moduleUri": "pkl:base" + }, + "properties": { + "minPklVersion": "0.26.0" + } + } + ] + } + """ + .trimIndent() + ) + } + private fun Path.zipFilePaths(): List { return FileSystems.newFileSystem(URI("jar:${toUri()}"), emptyMap()).use { fs -> Files.walk(fs.getPath("/")).map(IoUtils::toNormalizedPathString).collect(Collectors.toList()) diff --git a/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/deprecated@1.0.0.json b/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/deprecated@1.0.0.json new file mode 100644 index 00000000..f4cb3c76 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/deprecated@1.0.0.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:0/deprecated@1.0.0", + "name": "deprecated", + "version": "1.0.0", + "packageZipUrl": "https://localhost:0/deprecated@1.0.0/deprecated@1.0.0.zip", + "dependencies": {}, + "packageZipChecksums": { + "sha256": "$computedChecksum" + }, + "sourceCode": "https://example.com/deprecated", + "documentation": "https://example.com/deprecated-docs", + "license": "UNLICENSED", + "authors": [ + "deprecated@example.com" + ], + "issueTracker": "https://example.com/deprecated/issues", + "annotations": [ + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Deprecated", + "moduleUri": "pkl:base" + }, + "properties": { + "since": "1.0.0", + "message": "don't use", + "replaceWith": null + } + } + ] +} diff --git a/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/package/deprecated.pkl b/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/package/deprecated.pkl new file mode 100644 index 00000000..7d5a7442 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/deprecated@1.0.0/package/deprecated.pkl @@ -0,0 +1,9 @@ +/// A module from a deprecated package +module deprecated.deprecated + +/// Old foo +@Deprecated { replaceWith = "bar" } +foo: Int = 1 + +/// New bar +bar: Int = 2 diff --git a/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/package/unlisted.pkl b/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/package/unlisted.pkl new file mode 100644 index 00000000..cfd26fce --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/package/unlisted.pkl @@ -0,0 +1,5 @@ +/// No docs are actually generated for this module +module unlisted.unlisted + +/// 1 +foo: Int = 1 diff --git a/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/unlisted@1.0.0.json b/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/unlisted@1.0.0.json new file mode 100644 index 00000000..0dbfee90 --- /dev/null +++ b/pkl-commons-test/src/main/files/packages/unlisted@1.0.0/unlisted@1.0.0.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "packageUri": "package://localhost:0/unlisted@1.0.0", + "name": "unlisted", + "version": "1.0.0", + "packageZipUrl": "https://localhost:0/unlisted@1.0.0/unlisted@1.0.0.zip", + "dependencies": {}, + "packageZipChecksums": { + "sha256": "$computedChecksum" + }, + "sourceCode": "https://example.com/unlisted", + "documentation": "https://example.com/unlisted-docs", + "license": "UNLICENSED", + "authors": [ + "unlisted@example.com" + ], + "issueTracker": "https://example.com/unlisted/issues", + "annotations": [ + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Unlisted", + "moduleUri": "pkl:base" + }, + "properties": {} + } + ] +} diff --git a/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java b/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java index f5b0d5ff..0f76a53c 100644 --- a/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java +++ b/pkl-core/src/main/java/org/pkl/core/packages/DependencyMetadata.java @@ -19,12 +19,25 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.core.DataSize; +import org.pkl.core.DataSizeUnit; +import org.pkl.core.Duration; +import org.pkl.core.DurationUnit; +import org.pkl.core.PClassInfo; +import org.pkl.core.PNull; +import org.pkl.core.PObject; +import org.pkl.core.Pair; +import org.pkl.core.PklException; import org.pkl.core.Version; import org.pkl.core.packages.Dependency.RemoteDependency; import org.pkl.core.util.Nullable; @@ -33,6 +46,7 @@ import org.pkl.core.util.json.Json.FormatException; import org.pkl.core.util.json.Json.JsArray; import org.pkl.core.util.json.Json.JsObject; import org.pkl.core.util.json.Json.JsonParseException; +import org.pkl.core.util.json.Json.MissingFieldException; import org.pkl.core.util.json.JsonWriter; /** @@ -63,7 +77,25 @@ import org.pkl.core.util.json.JsonWriter; * "sha256": "abc123" * } * } - * } + * }, + * "annotations": [ + * { + * "moduleName": "pkl.base", + * "class": "Unlisted", + * "moduleUri": "pkl:base", + * "properties": {} + * }, + * { + * "moduleName": "pkl.base", + * "class": "Deprecated", + * "moduleUri": "pkl:base", + * "properties": { + * "since": "0.26.1", + * "message": "don't use", + * "replaceWith": null + * } + * } + * ] * } * * @@ -88,6 +120,8 @@ public final class DependencyMetadata { var authors = parsed.getNullable("authors", DependencyMetadata::parseAuthors); var issueTracker = parsed.getURIOrNull("issueTracker"); var description = parsed.getStringOrNull("description"); + var annotations = parsed.getNullable("annotations", DependencyMetadata::parseAnnotations); + if (annotations == null) annotations = List.of(); return new DependencyMetadata( name, packageUri, @@ -102,7 +136,8 @@ public final class DependencyMetadata { licenseText, authors, issueTracker, - description); + description, + annotations); } private static Map parseDependencies(Object deps) @@ -128,6 +163,122 @@ public final class DependencyMetadata { return ret; } + private static List parseAnnotations(Object ann) + throws JsonParseException, URISyntaxException { + if (!(ann instanceof JsArray arr)) { + throw new FormatException("array", ann.getClass()); + } + var annotations = new ArrayList(arr.size()); + for (var annotation : arr) { + var obj = parsePObject(annotation); + if (!(obj instanceof PObject pObject)) { + throw new PklException("Could not read annotation. Invalid object: " + obj); + } + annotations.add(pObject); + } + return annotations; + } + + private static Object parsePObject(@Nullable Object obj) + throws JsonParseException, URISyntaxException { + if (obj == null) { + return PNull.getInstance(); + } else if (obj instanceof String string) { + return string; + } else if (obj instanceof Boolean bool) { + return bool; + } else if (obj instanceof Integer integer) { + return integer.longValue(); + } else if (obj instanceof Long aLong) { + return aLong; + } else if (obj instanceof Float aFloat) { + return aFloat.doubleValue(); + } else if (obj instanceof Double aDouble) { + return aDouble; + } else if (obj instanceof JsArray array) { + var list = new ArrayList<>(array.size()); + for (var element : array) { + list.add(parsePObject(element)); + } + return list; + } else if (obj instanceof JsObject jsObj) { + var type = jsObj.getString("type"); + switch (type) { + case "Set" -> { + var value = jsObj.getArray("value"); + var set = new HashSet<>(value.size()); + for (var element : value) { + set.add(parsePObject(element)); + } + return set; + } + case "Map" -> { + var value = jsObj.getArray("value"); + var map = new HashMap<>(); + for (var kv : value) { + var kvObj = (JsObject) kv; + map.put(parsePObject(kvObj.get("key")), parsePObject(kvObj.get("value"))); + } + return map; + } + case "PObject" -> { + var classInfoObj = jsObj.getObject("classInfo"); + var moduleName = classInfoObj.getString("moduleName"); + var className = classInfoObj.getString("class"); + var moduleUri = classInfoObj.getString("moduleUri"); + var props = jsObj.getObject("properties"); + var classInfo = PClassInfo.get(moduleName, className, new URI(moduleUri)); + var properties = new HashMap(); + for (var kv : props.entrySet()) { + properties.put(kv.getKey(), parsePObject(kv.getValue())); + } + return new PObject(classInfo, properties); + } + case "Pattern" -> { + var value = jsObj.getString("value"); + return Pattern.compile(value); + } + case "DataSize" -> { + var symbol = jsObj.getString("unit"); + var value = jsObj.get("value"); + if (value == null) { + throw new MissingFieldException(jsObj, "value"); + } + var unit = DataSizeUnit.parse(symbol); + if (unit == null) { + throw new PklException("Invalid DataSize unit symbol: " + symbol); + } + if (!(value instanceof Double num)) { + throw new FormatException("double", value.getClass()); + } + return new DataSize(num, unit); + } + case "Duration" -> { + var symbol = jsObj.getString("unit"); + var value = jsObj.get("value"); + if (value == null) { + throw new MissingFieldException(jsObj, "value"); + } + var unit = DurationUnit.parse(symbol); + if (unit == null) { + throw new PklException("Invalid Duration unit symbol: " + symbol); + } + if (!(value instanceof Double num)) { + throw new FormatException("double", value.getClass()); + } + return new Duration(num, unit); + } + case "Pair" -> { + var first = parsePObject(jsObj.get("first")); + var second = parsePObject(jsObj.get("second")); + return new Pair<>(first, second); + } + } + } + // should never be reached + throw new PklException("Could not read annotation. Invalid object type: " + obj.getClass()); + } + public static Checksums parseChecksums(Object obj) throws JsonParseException { if (!(obj instanceof JsObject jsObj)) { throw new FormatException("object", obj.getClass()); @@ -164,6 +315,7 @@ public final class DependencyMetadata { private final @Nullable List authors; private final @Nullable URI issueTracker; private final @Nullable String description; + private final List annotations; public DependencyMetadata( String name, @@ -179,7 +331,8 @@ public final class DependencyMetadata { @Nullable String licenseText, @Nullable List authors, @Nullable URI issueTracker, - @Nullable String description) { + @Nullable String description, + List annotations) { this.name = name; this.packageUri = packageUri; this.version = version; @@ -194,6 +347,7 @@ public final class DependencyMetadata { this.authors = authors; this.issueTracker = issueTracker; this.description = description; + this.annotations = annotations; } public String getName() { @@ -250,6 +404,10 @@ public final class DependencyMetadata { return description; } + public List getAnnotations() { + return annotations; + } + /** Serializes project dependencies to JSON, and writes it to the provided output stream. */ public void writeTo(OutputStream out) throws IOException { new DependencyMetadataWriter(out, this).write(); @@ -277,7 +435,8 @@ public final class DependencyMetadata { && Objects.equals(licenseText, that.licenseText) && Objects.equals(authors, that.authors) && Objects.equals(issueTracker, that.issueTracker) - && Objects.equals(description, that.description); + && Objects.equals(description, that.description) + && Objects.equals(annotations, that.annotations); } @Override @@ -296,7 +455,8 @@ public final class DependencyMetadata { licenseText, authors, issueTracker, - description); + description, + annotations); } @Override @@ -339,6 +499,8 @@ public final class DependencyMetadata { + '\'' + ", description=" + description + + ", annotations=" + + annotations + '}'; } @@ -423,8 +585,125 @@ public final class DependencyMetadata { if (dependencyMetadata.description != null) { jsonWriter.name("description").value(dependencyMetadata.description); } + if (!dependencyMetadata.annotations.isEmpty()) { + jsonWriter.name("annotations"); + writeAnnotations(); + } jsonWriter.endObject(); jsonWriter.close(); } + + private void writeAnnotations() throws IOException { + jsonWriter.beginArray(); + for (var annotation : dependencyMetadata.annotations) { + writePObject(annotation); + } + jsonWriter.endArray(); + } + + private void writePClassInfo(PClassInfo pClassInfo) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("moduleName").value(pClassInfo.getModuleName()); + jsonWriter.name("class").value(pClassInfo.getSimpleName()); + jsonWriter.name("moduleUri").value(pClassInfo.getModuleUri().toString()); + jsonWriter.endObject(); + } + + private void writePObject(PObject object) throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("type").value("PObject"); + jsonWriter.name("classInfo"); + writePClassInfo(object.getClassInfo()); + jsonWriter.name("properties"); + jsonWriter.beginObject(); + for (var kv : object.getProperties().entrySet()) { + jsonWriter.name(kv.getKey()); + writeGenericObject(kv.getValue()); + } + jsonWriter.endObject(); + jsonWriter.endObject(); + } + + private void writeGenericObject(Object value) throws IOException { + if (value instanceof PNull) { + jsonWriter.nullValue(); + } else if (value instanceof PObject pObject) { + writePObject(pObject); + } else if (value instanceof String string) { + jsonWriter.value(string); + } else if (value instanceof Boolean bool) { + jsonWriter.value(bool); + } else if (value instanceof Integer num) { + jsonWriter.value(num); + } else if (value instanceof Long num) { + jsonWriter.value(num); + } else if (value instanceof Float num) { + jsonWriter.value(num); + } else if (value instanceof Double num) { + jsonWriter.value(num); + } else if (value instanceof List list) { + jsonWriter.beginArray(); + for (var v : list) { + writeGenericObject(v); + } + jsonWriter.endArray(); + } else if (value instanceof Set set) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("Set"); + jsonWriter.name("value"); + jsonWriter.beginArray(); + for (var v : set) { + writeGenericObject(v); + } + jsonWriter.endArray(); + jsonWriter.endObject(); + } else if (value instanceof Map map) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("Map"); + jsonWriter.name("value"); + jsonWriter.beginArray(); + for (var kv : map.entrySet()) { + jsonWriter.beginObject(); + jsonWriter.name("key"); + writeGenericObject(kv.getKey()); + jsonWriter.name("value"); + writeGenericObject(kv.getValue()); + jsonWriter.endObject(); + } + jsonWriter.endArray(); + jsonWriter.endObject(); + } else if (value instanceof Pattern pattern) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("Pattern"); + jsonWriter.name("value").value(pattern.pattern()); + jsonWriter.endObject(); + } else if (value instanceof DataSize dataSize) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("DataSize"); + jsonWriter.name("unit").value(dataSize.getUnit().getSymbol()); + jsonWriter.name("value").value(dataSize.getValue()); + jsonWriter.endObject(); + } else if (value instanceof Duration duration) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("Duration"); + jsonWriter.name("unit").value(duration.getUnit().getSymbol()); + jsonWriter.name("value").value(duration.getValue()); + jsonWriter.endObject(); + } else if (value instanceof Pair pair) { + jsonWriter.beginObject(); + jsonWriter.name("type").value("Pair"); + jsonWriter.name("first"); + writeGenericObject(pair.getFirst()); + jsonWriter.name("second"); + writeGenericObject(pair.getSecond()); + jsonWriter.endObject(); + } else { + // PClass and TypeAlias are not supported + throw new PklException( + "Error serializing annotation for PklProject:\n:" + + " cannot render value with unexpected type: " + + value.getClass()); + } + } } } diff --git a/pkl-core/src/main/java/org/pkl/core/project/Project.java b/pkl-core/src/main/java/org/pkl/core/project/Project.java index d0fc8341..0d15ec33 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/Project.java +++ b/pkl-core/src/main/java/org/pkl/core/project/Project.java @@ -65,6 +65,7 @@ public final class Project { private final URI projectBaseUri; private final List tests; private final Map localProjectDependencies; + private final List annotations; /** * Loads Project data from the given {@link Path}. @@ -256,6 +257,11 @@ public final class Project { return new RemoteDependency(packageUri, checksums); } + @SuppressWarnings("unchecked") + private static List parseAnnotations(PObject module) { + return (List) getProperty(module, "annotations"); + } + public static Project parseProject(PObject module) throws URISyntaxException { var pkgObj = getNullableProperty(module, "package"); var projectFileUri = URI.create((String) module.getProperty("projectFileUri")); @@ -279,6 +285,7 @@ public final class Project { .map((it) -> projectBaseUri.resolve(it).normalize()) .collect(Collectors.toList()); var localProjectDependencies = parseLocalProjectDependencies(module); + var annotations = parseAnnotations(module); return new Project( pkg, dependencies, @@ -286,7 +293,8 @@ public final class Project { projectFileUri, projectBaseUri, tests, - localProjectDependencies); + localProjectDependencies, + annotations); } private static Map parseLocalProjectDependencies(PObject module) @@ -399,7 +407,8 @@ public final class Project { URI projectFileUri, URI projectBaseUri, List tests, - Map localProjectDependencies) { + Map localProjectDependencies, + List annotations) { this.pkg = pkg; this.dependencies = dependencies; this.evaluatorSettings = evaluatorSettings; @@ -407,6 +416,7 @@ public final class Project { this.projectBaseUri = projectBaseUri; this.tests = tests; this.localProjectDependencies = localProjectDependencies; + this.annotations = annotations; } public @Nullable Package getPackage() { @@ -453,12 +463,13 @@ public final class Project { && dependencies.equals(project.dependencies) && evaluatorSettings.equals(project.evaluatorSettings) && projectFileUri.equals(project.projectFileUri) - && tests.equals(project.tests); + && tests.equals(project.tests) + && annotations.equals(project.annotations); } @Override public int hashCode() { - return Objects.hash(pkg, dependencies, evaluatorSettings, projectFileUri, tests); + return Objects.hash(pkg, dependencies, evaluatorSettings, projectFileUri, tests, annotations); } public DeclaredDependencies getDependencies() { @@ -478,6 +489,10 @@ public final class Project { return Path.of(projectBaseUri); } + public List getAnnotations() { + return annotations; + } + @Deprecated(forRemoval = true) public static class EvaluatorSettings { private final PklEvaluatorSettings delegate; diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java index a0611322..f5fd79f1 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java @@ -288,7 +288,8 @@ public final class ProjectPackager { pkg.getLicenseText(), pkg.getAuthors(), pkg.getIssueTracker(), - pkg.getDescription()); + pkg.getDescription(), + project.getAnnotations()); } private DigestOutputStream newDigestOutputStream(OutputStream outputStream) { diff --git a/pkl-core/src/test/kotlin/org/pkl/core/packages/DependencyMetadataTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/packages/DependencyMetadataTest.kt index b21d947e..6d4999b2 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/packages/DependencyMetadataTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/packages/DependencyMetadataTest.kt @@ -18,9 +18,10 @@ package org.pkl.core.packages import java.io.ByteArrayOutputStream import java.net.URI import java.nio.charset.StandardCharsets +import java.util.regex.Pattern import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.pkl.core.Version +import org.pkl.core.* class DependencyMetadataTest { private val dependencyMetadata = @@ -44,9 +45,28 @@ class DependencyMetadataTest { "The MIT License, you know it", listOf("birdy@bird.com"), URI("https://example.com/issues"), - "Some package description" + "Some package description", + listOf( + PObject(PClassInfo.Unlisted, mapOf()), + PObject(PClassInfo.Deprecated, mapOf("since" to "0.26.1", "message" to "don't use")), + PObject( + PClassInfo.get("myModule", "MyAnnotation", URI("pkl:fake")), + mapOf( + "string" to "bar", + "boolean" to true, + "long" to 1L, + "double" to 1.66, + "null" to PNull.getInstance(), + "list" to listOf("a", "b"), + "set" to setOf("a", "b"), + "map" to mapOf(true to "t", false to "f"), + "dataSize" to DataSize(1.5, DataSizeUnit.GIGABYTES), + "duration" to Duration(2.9, DurationUnit.HOURS), + "pair" to Pair(1L, "1") + ) + ) + ), ) - private val dependencyMetadataStr = """ { @@ -74,7 +94,84 @@ class DependencyMetadataTest { "birdy@bird.com" ], "issueTracker": "https://example.com/issues", - "description": "Some package description" + "description": "Some package description", + "annotations": [ + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Unlisted", + "moduleUri": "pkl:base" + }, + "properties": {} + }, + { + "type": "PObject", + "classInfo": { + "moduleName": "pkl.base", + "class": "Deprecated", + "moduleUri": "pkl:base" + }, + "properties": { + "since": "0.26.1", + "message": "don't use" + } + }, + { + "type": "PObject", + "classInfo": { + "moduleName": "myModule", + "class": "MyAnnotation", + "moduleUri": "pkl:fake" + }, + "properties": { + "string": "bar", + "boolean": true, + "long": 1, + "double": 1.66, + "null": null, + "list": [ + "a", + "b" + ], + "set": { + "type": "Set", + "value": [ + "a", + "b" + ] + }, + "map": { + "type": "Map", + "value": [ + { + "key": true, + "value": "t" + }, + { + "key": false, + "value": "f" + } + ] + }, + "dataSize": { + "type": "DataSize", + "unit": "gb", + "value": 1.5 + }, + "duration": { + "type": "Duration", + "unit": "h", + "value": 2.9 + }, + "pair": { + "type": "Pair", + "first": 1, + "second": "1" + } + } + } + ] } """ .trimIndent() @@ -85,6 +182,87 @@ class DependencyMetadataTest { assertThat(parsed).isEqualTo(dependencyMetadata) } + /** Patterns cannot be compared with [equals], so we have to test them separately. */ + @Test + fun testPatternSerialization() { + val dependencyMetadata = + DependencyMetadata( + "my-proj-name", + PackageUri("package://example.com/my-proj-name@0.10.0"), + Version.parse("0.10.0"), + URI("https://example.com/foo/bar@0.5.3.zip"), + Checksums("abc123"), + mapOf(), + "https://example.com/my/source/0.5.3/blob%{path}#L%{line}-L%{endLine}", + URI("https://example.com/my/source"), + URI("https://example.com/my/docs"), + "MIT", + "The MIT License, you know it", + listOf("birdy@bird.com"), + URI("https://example.com/issues"), + "Some package description", + listOf( + PObject( + PClassInfo.get("myModule", "MyAnnotation", URI("pkl:fake")), + mapOf("pattern" to Regex(".*").toPattern()) + ) + ), + ) + val dependencyMetadataStr = + """ + { + "name": "my-proj-name", + "packageUri": "package://example.com/my-proj-name@0.10.0", + "version": "0.10.0", + "packageZipUrl": "https://example.com/foo/bar@0.5.3.zip", + "packageZipChecksums": { + "sha256": "abc123" + }, + "dependencies": {}, + "sourceCodeUrlScheme": "https://example.com/my/source/0.5.3/blob%{path}#L%{line}-L%{endLine}", + "sourceCode": "https://example.com/my/source", + "documentation": "https://example.com/my/docs", + "license": "MIT", + "licenseText": "The MIT License, you know it", + "authors": [ + "birdy@bird.com" + ], + "issueTracker": "https://example.com/issues", + "description": "Some package description", + "annotations": [ + { + "type": "PObject", + "classInfo": { + "moduleName": "myModule", + "class": "MyAnnotation", + "moduleUri": "pkl:fake" + }, + "properties": { + "pattern": { + "type": "Pattern", + "value": ".*" + } + } + } + ] + } + """ + .trimIndent() + + val parsed = DependencyMetadata.parse(dependencyMetadataStr) + val expectedPattern = dependencyMetadata.annotations[0]["pattern"] as Pattern + val actualPattern = parsed.annotations[0]["pattern"] + assertThat(actualPattern).isInstanceOf(Pattern::class.java) + actualPattern as Pattern + assertThat(expectedPattern.pattern()).isEqualTo(actualPattern.pattern()) + + val str = + ByteArrayOutputStream() + .apply { dependencyMetadata.writeTo(this) } + .toString(StandardCharsets.UTF_8) + assertThat(str).isEqualTo(dependencyMetadataStr) + } + @Test fun writeTo() { val str = diff --git a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt index b60a75b2..5961375e 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt @@ -72,8 +72,20 @@ class ProjectTest { path, null ) + val expectedAnnotations = + listOf( + PObject( + PClassInfo.Deprecated, + mapOf("since" to "1.2", "message" to "do not use", "replaceWith" to "somethingElse") + ), + PObject(PClassInfo.Unlisted, mapOf()), + PObject(PClassInfo.ModuleInfo, mapOf("minPklVersion" to "0.26.0")), + ) projectPath.writeString( """ + @Deprecated { since = "1.2"; message = "do not use"; replaceWith = "somethingElse" } + @Unlisted + @ModuleInfo { minPklVersion = "0.26.0" } amends "pkl:Project" evaluatorSettings { @@ -138,6 +150,7 @@ class ProjectTest { val project = Project.loadFromPath(projectPath) assertThat(project.`package`).isEqualTo(expectedPackage) assertThat(project.evaluatorSettings).isEqualTo(expectedSettings) + assertThat(project.annotations).isEqualTo(expectedAnnotations) assertThat(project.tests) .isEqualTo(listOf(path.resolve("test1.pkl"), path.resolve("test2.pkl"))) } diff --git a/pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGenerator.kt b/pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGenerator.kt index 422faff4..4ae7605f 100644 --- a/pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGenerator.kt +++ b/pkl-doc/src/main/kotlin/org/pkl/doc/CliDocGenerator.kt @@ -108,7 +108,8 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand( overview = metadata.description, extraAttributes = mapOf("Checksum" to checksum.sha256), sourceCode = metadata.sourceCode, - sourceCodeUrlScheme = metadata.sourceCodeUrlScheme + sourceCodeUrlScheme = metadata.sourceCodeUrlScheme, + annotations = metadata.annotations, ) } diff --git a/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/deprecated/index.js b/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/deprecated/index.js new file mode 100644 index 00000000..018c3e63 --- /dev/null +++ b/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/deprecated/index.js @@ -0,0 +1 @@ +runtimeData.links('known-versions','[{"text":"1.0.0","classes":"current-version"}]'); diff --git a/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/index.js b/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/index.js new file mode 100644 index 00000000..018c3e63 --- /dev/null +++ b/pkl-doc/src/test/files/DocGeneratorTest/output/data/localhost(3a)0/deprecated/1.0.0/index.js @@ -0,0 +1 @@ +runtimeData.links('known-versions','[{"text":"1.0.0","classes":"current-version"}]'); diff --git a/pkl-doc/src/test/files/DocGeneratorTest/output/index.html b/pkl-doc/src/test/files/DocGeneratorTest/output/index.html index 12c3df64..a5406c2e 100644 --- a/pkl-doc/src/test/files/DocGeneratorTest/output/index.html +++ b/pkl-doc/src/test/files/DocGeneratorTest/output/index.html @@ -118,6 +118,18 @@ age: Int +
  • +
    + +