From 73264e8fd11ee128cd02a7405b11e330d63b0dc8 Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Fri, 23 Jan 2026 12:44:41 -0800 Subject: [PATCH] SPICE-0024: `Annotation` converters (#1333) This enables defining declarative key and/or value transformations in cases where neither `Class`- nor path-based converters can be applied gracefully. It is also the only way to express transforming the resulting property names in `Typed` objects without applying a converter to the entire containing type, which is cumbersome at best. SPICE: https://github.com/apple/pkl-evolution/pull/26 --- .../pkl/core/ast/member/ClassProperty.java | 80 +++++++++--------- .../java/org/pkl/core/runtime/BaseModule.java | 10 ++- .../java/org/pkl/core/runtime/Identifier.java | 11 +-- .../org/pkl/core/runtime/MirrorFactories.java | 28 +++---- .../org/pkl/core/runtime/ModuleCache.java | 6 +- .../java/org/pkl/core/runtime/TestRunner.java | 2 +- .../java/org/pkl/core/runtime/VmClass.java | 6 +- .../pkl/core/runtime/VmPklBinaryEncoder.java | 4 +- .../pkl/core/runtime/VmValueConverter.java | 8 +- .../org/pkl/core/stdlib/AbstractRenderer.java | 23 ++++- .../org/pkl/core/stdlib/PklConverter.java | 81 ++++++++++++++++-- .../core/stdlib/base/JsonRendererNodes.java | 6 +- .../core/stdlib/base/PListRendererNodes.java | 6 +- .../org/pkl/core/stdlib/base/PcfRenderer.java | 2 +- .../core/stdlib/base/PcfRendererNodes.java | 10 ++- .../stdlib/base/PropertiesRendererNodes.java | 7 +- .../core/stdlib/base/YamlRendererNodes.java | 11 ++- .../org/pkl/core/stdlib/json/ParserNodes.java | 9 +- .../core/stdlib/jsonnet/RendererNodes.java | 6 +- .../core/stdlib/pklbinary/RendererNodes.java | 9 +- .../core/stdlib/protobuf/RendererNodes.java | 5 +- .../core/stdlib/test/report/JUnitReport.java | 3 +- .../pkl/core/stdlib/xml/RendererNodes.java | 11 ++- .../org/pkl/core/stdlib/yaml/ParserNodes.java | 11 +-- .../input-helper/api/annotationConverter.pkl | 19 +++++ .../input/api/annotationConverters.pkl | 63 ++++++++++++++ .../input/api/jsonRenderer9.json5.pkl | 27 ++++++ .../input/api/jsonnetRenderer8.jsonnet.pkl | 26 ++++++ .../input/api/pListRenderer8.plist.pkl | 23 +++++ .../input/api/pcfRenderer9.pkl | 23 +++++ .../api/propertiesRenderer12.properties.pkl | 23 +++++ .../input/api/protobuf3.txtpb.pkl | 28 +++++++ .../input/api/xmlRenderer9.xml.pkl | 26 ++++++ .../input/api/yamlRenderer10.yml.pkl | 41 +++++++++ .../output/api/annotationConverters.pcf | 5 ++ .../output/api/jsonRenderer9.json5 | 8 ++ .../output/api/jsonnetRenderer8.jsonnet | 8 ++ .../output/api/pListRenderer8.plist | 17 ++++ .../output/api/pcfRenderer9.pcf | 6 ++ .../output/api/pklbinary1.msgpack.yaml | 6 ++ .../api/propertiesRenderer12.properties | 6 ++ .../output/api/protobuf3.txtpb | 6 ++ .../output/api/xmlRenderer9.xml | 9 ++ .../output/api/yamlRenderer10.yml | 6 ++ stdlib/base.pkl | 83 ++++++++++++++++++- stdlib/json.pkl | 10 +++ stdlib/jsonnet.pkl | 20 +++++ stdlib/pklbinary.pkl | 12 +++ stdlib/protobuf.pkl | 20 +++++ stdlib/xml.pkl | 28 ++++++- stdlib/yaml.pkl | 10 +++ 51 files changed, 773 insertions(+), 141 deletions(-) create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5 create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java index 6a2ee637..59d3b978 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassProperty.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -54,6 +54,44 @@ public final class ClassProperty extends ClassMember { this.initializer = initializer; } + public List getAllAnnotations(boolean ascending) { + var annotations = new ArrayList(); + + if (ascending) { + for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) { + var p = clazz.getDeclaredProperty(getName()); + if (p != null) { + annotations.addAll(p.getAnnotations()); + } + } + } else { + doGetAllAnnotationsDescending(getDeclaringClass(), annotations); + } + + return annotations; + } + + private void doGetAllAnnotationsDescending(VmClass clazz, List annotations) { + if (clazz.getSuperclass() != null) { + doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations); + } + var p = clazz.getDeclaredProperty(getName()); + if (p != null) { + annotations.addAll(p.getAnnotations()); + } + } + + public VmSet getAllModifierMirrors() { + var mods = 0; + for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) { + var parent = clazz.getDeclaredProperty(getName()); + if (parent != null) { + mods |= parent.getModifiers(); + } + } + return VmModifier.getMirrors(mods, false); + } + public @Nullable PropertyTypeNode getTypeNode() { return typeNode; } @@ -68,44 +106,8 @@ public final class ClassProperty extends ClassMember { return name.toString(); } - public static final class Mirror { - private final ClassProperty prop; - private final VmClass clazz; - - Mirror(ClassProperty prop, VmClass clazz) { - this.prop = prop; - this.clazz = clazz; - } - - public ClassProperty getProperty() { - return prop; - } - - public List getAllAnnotations() { - var annotations = new ArrayList(); - for (var klazz = clazz; klazz != null; klazz = klazz.getSuperclass()) { - var p = klazz.getDeclaredProperty(prop.getName()); - if (p != null) { - annotations.addAll(p.getAnnotations()); - } - } - return annotations; - } - - public VmSet getAllModifierMirrors() { - var mods = 0; - for (var klazz = clazz; klazz != null; klazz = klazz.getSuperclass()) { - var parent = klazz.getDeclaredProperty(prop.getName()); - if (parent != null) { - mods |= parent.getModifiers(); - } - } - return VmModifier.getMirrors(mods, false); - } - } - - public VmTyped getMirror(VmClass clazz) { - return MirrorFactories.propertyFactory.create(new Mirror(this, clazz)); + public VmTyped getMirror() { + return MirrorFactories.propertyFactory.create(this); } public VmSet getModifierMirrors() { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java index f0497dd5..597cca14 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/BaseModule.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -195,6 +195,10 @@ public final class BaseModule extends StdLibModule { return AnnotationClass.instance; } + public static VmClass getConvertPropertyClass() { + return ConvertPropertyClass.instance; + } + public static VmClass getDeprecatedClass() { return DeprecatedClass.instance; } @@ -343,6 +347,10 @@ public final class BaseModule extends StdLibModule { static final VmClass instance = loadClass("Annotation"); } + private static final class ConvertPropertyClass { + static final VmClass instance = loadClass("ConvertProperty"); + } + private static final class DeprecatedClass { static final VmClass instance = loadClass("Deprecated"); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java index 8acfc3da..732beb76 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/Identifier.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -50,7 +50,7 @@ public final class Identifier implements Comparable { // members of pkl.base#Listing and pkl.base#Mapping public static final Identifier DEFAULT = get("default"); - // members of pkl.base#ValueRenderer subclasses + // members of pkl.base#BaseValueRenderer subclasses public static final Identifier MODE = get("mode"); public static final Identifier INDENT = get("indent"); public static final Identifier INDENT_WIDTH = get("indentWidth"); @@ -62,8 +62,12 @@ public final class Identifier implements Comparable { public static final Identifier ROOT_ELEMENT_NAME = get("rootElementName"); public static final Identifier ROOT_ELEMENT_ATTRIBUTES = get("rootElementAttributes"); public static final Identifier CONVERTERS = get("converters"); + public static final Identifier CONVERT_PROPERTY_TRANSFORMERS = get("convertPropertyTransformers"); public static final Identifier USE_MAPPING = get("useMapping"); + // members of pkl.base#ConvertProperty + public static final Identifier RENDER = get("render"); + // members of pkl.base#RegexMatch public static final Identifier VALUE = get("value"); @@ -143,9 +147,6 @@ public final class Identifier implements Comparable { // members of pkl.yaml public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases"); - // members of pkl.encoding - public static final Identifier IMPORTS = get("imports"); - // common in lambdas etc public static final Identifier IT = get("it"); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java b/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java index 5078ff7b..fedcf38b 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/MirrorFactories.java @@ -37,7 +37,7 @@ public final class MirrorFactories { public static final VmObjectFactory typeAliasFactory = new VmObjectFactory<>(ReflectModule::getTypeAliasClass); - public static final VmObjectFactory propertyFactory = + public static final VmObjectFactory propertyFactory = new VmObjectFactory<>(ReflectModule::getPropertyClass); public static final VmObjectFactory methodFactory = @@ -166,33 +166,29 @@ public final class MirrorFactories { propertyFactory .addTypedProperty( - "location", - property -> sourceLocationFactory.create(property.getProperty().getHeaderSection())) + "location", property -> sourceLocationFactory.create(property.getHeaderSection())) .addProperty( "docComment", - property -> - VmNull.lift(VmUtils.exportDocComment(property.getProperty().getDocComment()))) + property -> VmNull.lift(VmUtils.exportDocComment(property.getDocComment()))) + .addListProperty("annotations", property -> VmList.create(property.getAnnotations())) .addListProperty( - "annotations", property -> VmList.create(property.getProperty().getAnnotations())) - .addListProperty("allAnnotations", property -> VmList.create(property.getAllAnnotations())) - .addSetProperty("modifiers", property -> property.getProperty().getModifierMirrors()) - .addSetProperty("allModifiers", ClassProperty.Mirror::getAllModifierMirrors) - .addStringProperty("name", property -> property.getProperty().getName().toString()) - .addTypedProperty("type", property -> property.getProperty().getTypeMirror()) + "allAnnotations", property -> VmList.create(property.getAllAnnotations(true))) + .addSetProperty("modifiers", ClassProperty::getModifierMirrors) + .addSetProperty("allModifiers", ClassProperty::getAllModifierMirrors) + .addStringProperty("name", property -> property.getName().toString()) + .addTypedProperty("type", ClassProperty::getTypeMirror) .addProperty( "defaultValue", property -> - property.getProperty().isAbstract() - || property.getProperty().isExternal() + property.isAbstract() + || property.isExternal() || property - .getProperty() .getInitializer() .isUndefined(VmUtils.createEmptyMaterializedFrame()) ? VmNull.withoutDefault() : // get default from prototype because it's cached there - VmUtils.readMember( - property.getProperty().getOwner(), property.getProperty().getName())); + VmUtils.readMember(property.getOwner(), property.getName())); methodFactory .addTypedProperty( diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java index 90ceaa3f..3293b661 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -92,12 +92,12 @@ public final class ModuleCache { return BaseModule.getModule(); case "Benchmark": return BenchmarkModule.getModule(); - case "pklbinary": - return PklBinaryModule.getModule(); case "jsonnet": return JsonnetModule.getModule(); case "math": return MathModule.getModule(); + case "pklbinary": + return PklBinaryModule.getModule(); case "platform": return PlatformModule.getModule(); case "project": diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java index e416f088..c613dcec 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/TestRunner.java @@ -44,7 +44,7 @@ import org.pkl.core.util.MutableReference; /** Runs test results examples and facts. */ public final class TestRunner { - private static final PklConverter converter = new PklConverter(VmMapping.empty()); + private static final PklConverter converter = PklConverter.NOOP; private final BufferedLogger logger; private final StackFrameTransformer stackFrameTransformer; private final boolean overwrite; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java index 08d44f50..7d1e3880 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmClass.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -575,7 +575,7 @@ public final class VmClass extends VmValue { var builder = VmMap.builder(); for (var property : declaredProperties.getValues()) { if (property.isLocal()) continue; - builder.add(property.getName().toString(), property.getMirror(this)); + builder.add(property.getName().toString(), property.getMirror()); } return builder.build(); } @@ -584,7 +584,7 @@ public final class VmClass extends VmValue { var builder = VmMap.builder(); for (var property : getAllProperties().getValues()) { if (property.isLocal()) continue; - builder.add(property.getName().toString(), property.getMirror(this)); + builder.add(property.getName().toString(), property.getMirror()); } return builder.build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java index ec7542aa..0c8cbde4 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmPklBinaryEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-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. @@ -38,7 +38,7 @@ public class VmPklBinaryEncoder extends AbstractRenderer { } public VmPklBinaryEncoder(MessageBufferPacker packer) { - this(packer, new PklConverter(VmMapping.empty())); + this(packer, PklConverter.NOOP); } private void packCode(PklBinaryCode code) throws IOException { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java index 3745daf0..62f2696f 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmValueConverter.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -15,6 +15,9 @@ */ package org.pkl.core.runtime; +import org.pkl.core.ast.member.ClassProperty; +import org.pkl.core.util.Pair; + public interface VmValueConverter { Object WILDCARD_PROPERTY = new Object() { @@ -82,6 +85,9 @@ public interface VmValueConverter { T convertFunction(VmFunction value, Iterable path); + /** Returns with an empty identifier if the second value is a RenderDirective */ + Pair convertProperty(ClassProperty property, Object value, Iterable path); + default T convert(Object value, Iterable path) { if (value instanceof VmValue vmValue) { return vmValue.accept(this, path); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java index de6e3d62..4fedf8ff 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/AbstractRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -19,6 +19,7 @@ import com.oracle.truffle.api.source.SourceSection; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; +import org.pkl.core.ast.member.ClassProperty; import org.pkl.core.runtime.BaseModule; import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmClass; @@ -195,7 +196,12 @@ public abstract class AbstractRenderer implements VmValueVisitor { (memberKey, member, memberValue) -> { if (member.isClass() || member.isTypeAlias()) return true; assert member.isProp(); - doVisitProperty((Identifier) memberKey, memberValue, member.getSourceSection(), isFirst); + doVisitProperty( + (Identifier) memberKey, + memberValue, + value.getVmClass().getProperty((Identifier) memberKey), + member.getSourceSection(), + isFirst); return true; }); @@ -218,7 +224,7 @@ public abstract class AbstractRenderer implements VmValueVisitor { var sourceSection = member.getSourceSection(); if (member.isProp()) { if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); - doVisitProperty((Identifier) memberKey, memberValue, sourceSection, isFirst); + doVisitProperty((Identifier) memberKey, memberValue, null, sourceSection, isFirst); } else if (member.isEntry()) { if (!canRenderPropertyOrEntry) cannotRenderObjectWithElementsAndOtherMembers(value); doVisitEntry(memberKey, memberValue, sourceSection, isFirst); @@ -327,10 +333,19 @@ public abstract class AbstractRenderer implements VmValueVisitor { } private void doVisitProperty( - Identifier name, Object value, SourceSection sourceSection, MutableBoolean isFirst) { + Identifier name, + Object value, + @Nullable ClassProperty classProperty, + SourceSection sourceSection, + MutableBoolean isFirst) { var prevSourceSection = currSourceSection; currSourceSection = sourceSection; currPath.push(name); + if (classProperty != null) { + var propVal = converter.convertProperty(classProperty, value, currPath); + name = propVal.getFirst(); + value = propVal.getSecond(); + } var convertedValue = converter.convert(value, currPath); if (!(skipNullProperties && convertedValue instanceof VmNull)) { visitProperty(name, convertedValue, isFirst.getAndSetFalse()); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java index ee914345..53a225a9 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/PklConverter.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -16,13 +16,16 @@ package org.pkl.core.stdlib; import java.util.*; +import org.pkl.core.ast.member.ClassProperty; import org.pkl.core.runtime.*; import org.pkl.core.util.Nullable; import org.pkl.core.util.Pair; public final class PklConverter implements VmValueConverter { private final Map typeConverters; + private final Map convertPropertyTransformers; private final Pair[] pathConverters; + private final Object rendererOrParser; private final @Nullable VmFunction stringConverter; private final @Nullable VmFunction booleanConverter; @@ -44,12 +47,15 @@ public final class PklConverter implements VmValueConverter { private final @Nullable VmFunction classConverter; private final @Nullable VmFunction typeAliasConverter; - public PklConverter(VmMapping converters) { - // As of 0.18, `converters` is forced by the mapping type check, - // but let's not rely on this implementation detail. + private PklConverter( + VmMapping converters, VmMapping convertPropertyTransformers, Object rendererOrParser) { converters.force(false, false); + convertPropertyTransformers.force(false, false); typeConverters = createTypeConverters(converters); + this.convertPropertyTransformers = + createConvertPropertyTransformers(convertPropertyTransformers); pathConverters = createPathConverters(converters); + this.rendererOrParser = rendererOrParser; stringConverter = typeConverters.get(BaseModule.getStringClass()); booleanConverter = typeConverters.get(BaseModule.getBooleanClass()); @@ -72,6 +78,22 @@ public final class PklConverter implements VmValueConverter { typeAliasConverter = typeConverters.get(BaseModule.getTypeAliasClass()); } + public static final PklConverter NOOP = + new PklConverter(VmMapping.empty(), VmMapping.empty(), VmNull.withoutDefault()); + + public static PklConverter fromRenderer(VmTyped renderer) { + var converters = (VmMapping) VmUtils.readMember(renderer, Identifier.CONVERTERS); + var convertPropertyTransformers = + (VmMapping) VmUtils.readMember(renderer, Identifier.CONVERT_PROPERTY_TRANSFORMERS); + return new PklConverter(converters, convertPropertyTransformers, renderer); + } + + public static PklConverter fromParser(VmTyped parser) { + var converters = (VmMapping) VmUtils.readMember(parser, Identifier.CONVERTERS); + return new PklConverter( + converters, VmMapping.empty(), parser); // no annotation converters in parsers + } + @Override public Object convertString(String value, Iterable path) { return doConvert(value, path, stringConverter); @@ -177,6 +199,34 @@ public final class PklConverter implements VmValueConverter { return doConvert(value, path, nullConverter); } + @Override + public Pair convertProperty( + ClassProperty property, Object value, Iterable path) { + var name = property.getName(); + + var annotations = property.getAllAnnotations(false); + if (annotations.isEmpty()) { + return Pair.of(name, value); + } + + var prop = new VmPair(name.toString(), value); + for (var annotation : annotations) { + if (!annotation.getVmClass().isSubclassOf(BaseModule.getConvertPropertyClass())) { + continue; + } + + var transformer = findConvertPropertyTransformer(annotation.getVmClass()); + if (transformer != null) { + annotation = (VmTyped) transformer.apply(annotation); + } + + var renderFunction = (VmFunction) VmUtils.readMember(annotation, Identifier.RENDER); + prop = (VmPair) renderFunction.apply(prop, rendererOrParser); + } + + return Pair.of(Identifier.get((String) prop.getFirst()), prop.getSecond()); + } + private Map createTypeConverters(VmMapping converters) { var result = new HashMap(); converters.iterateMemberValues( @@ -190,6 +240,19 @@ public final class PklConverter implements VmValueConverter { return result; } + private Map createConvertPropertyTransformers( + VmMapping convertPropertyTransformers) { + var result = new HashMap(); + convertPropertyTransformers.iterateMemberValues( + (key, member, value) -> { + assert value != null; // forced in ctor + result.put((VmClass) key, ((VmFunction) value)); + return true; + }); + + return result; + } + @SuppressWarnings("unchecked") private Pair[] createPathConverters(VmMapping converters) { var result = new ArrayList>(); @@ -221,8 +284,16 @@ public final class PklConverter implements VmValueConverter { * method will return the most specific converter for a type. */ private @Nullable VmFunction findTypeConverter(VmClass clazz) { + return findConverterByType(typeConverters, clazz); + } + + private @Nullable VmFunction findConvertPropertyTransformer(VmClass clazz) { + return findConverterByType(convertPropertyTransformers, clazz); + } + + private @Nullable T findConverterByType(Map bag, VmClass clazz) { for (var current = clazz; current != null; current = current.getSuperclass()) { - var found = typeConverters.get(current); + var found = bag.get(current); if (found != null) return found; } return null; diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java index eff053fe..5faf4b39 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/JsonRendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -52,9 +52,7 @@ public final class JsonRendererNodes { private static JsonRenderer createRenderer(VmTyped self, StringBuilder builder) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); - return new JsonRenderer(builder, indent, converter, omitNullProperties); + return new JsonRenderer(builder, indent, PklConverter.fromRenderer(self), omitNullProperties); } private static final class JsonRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java index f2e73370..ef64dc08 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PListRendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -48,9 +48,7 @@ public final class PListRendererNodes { private static PListRenderer createRenderer(VmTyped self, StringBuilder builder) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); - return new PListRenderer(builder, indent, converter); + return new PListRenderer(builder, indent, PklConverter.fromRenderer(self)); } // keep in sync with org.pkl.core.PListRenderer diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRenderer.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRenderer.java index 62995f7c..bdfa7214 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java index c7551638..607c1ecb 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PcfRendererNodes.java @@ -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. @@ -46,12 +46,14 @@ public final class PcfRendererNodes { private static PcfRenderer createRenderer(VmTyped self, StringBuilder builder) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); var useCustomStringDelimiters = (boolean) VmUtils.readMember(self, Identifier.USE_CUSTOM_STRING_DELIMITERS); - var converter = new PklConverter(converters); return new PcfRenderer( - builder, indent, converter, omitNullProperties, useCustomStringDelimiters); + builder, + indent, + PklConverter.fromRenderer(self), + omitNullProperties, + useCustomStringDelimiters); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java index 31ad32ff..c74cf5f6 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/PropertiesRendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -69,9 +69,8 @@ public final class PropertiesRendererNodes { private static PropertiesRenderer createRenderer(VmTyped self, StringBuilder builder) { var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); var restrictCharset = (boolean) VmUtils.readMember(self, Identifier.RESTRICT_CHARSET); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var PklConverter = new PklConverter(converters); - return new PropertiesRenderer(builder, omitNullProperties, restrictCharset, PklConverter); + return new PropertiesRenderer( + builder, omitNullProperties, restrictCharset, PklConverter.fromRenderer(self)); } private static final class PropertiesRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java index d95ba277..2259c27f 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/base/YamlRendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -69,10 +69,13 @@ public final class YamlRendererNodes { var indentWidth = ((Long) VmUtils.readMember(self, Identifier.INDENT_WIDTH)).intValue(); var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); var isStream = (boolean) VmUtils.readMember(self, Identifier.IS_STREAM); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); return new YamlRenderer( - builder, " ".repeat(indentWidth), converter, omitNullProperties, mode, isStream); + builder, + " ".repeat(indentWidth), + PklConverter.fromRenderer(self), + omitNullProperties, + mode, + isStream); } private static final class YamlRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java index 1321674d..c0fd2ae3 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/json/ParserNodes.java @@ -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. @@ -51,7 +51,7 @@ public final class ParserNodes { } private Object doParse(VmTyped self, String text) { - var converter = createConverter(self); + var converter = PklConverter.fromParser(self); var useMapping = (boolean) VmUtils.readMember(self, Identifier.USE_MAPPING); var handler = new Handler(converter, useMapping); var parser = new JsonParser(handler); @@ -64,11 +64,6 @@ public final class ParserNodes { } } - private static PklConverter createConverter(VmTyped self) { - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - return new PklConverter(converters); - } - private static class Handler extends JsonHandler, EconomicMap> { private final PklConverter converter; diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java index ee3b8ca4..725b56ab 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/jsonnet/RendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -47,9 +47,7 @@ public final class RendererNodes { var indent = (String) VmNull.unwrap(VmUtils.readMember(self, Identifier.INDENT)); if (indent == null) indent = ""; var omitNullProperties = (boolean) VmUtils.readMember(self, Identifier.OMIT_NULL_PROPERTIES); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); - return new Renderer(builder, indent, omitNullProperties, converter); + return new Renderer(builder, indent, omitNullProperties, PklConverter.fromRenderer(self)); } public abstract static class renderDocument extends ExternalMethod1Node { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java index 30009733..3c09d771 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/pklbinary/RendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved. + * Copyright © 2025-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. @@ -19,12 +19,9 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Specialization; import org.msgpack.core.MessageBufferPacker; import org.msgpack.core.MessagePack; -import org.pkl.core.runtime.Identifier; import org.pkl.core.runtime.VmBytes; -import org.pkl.core.runtime.VmMapping; import org.pkl.core.runtime.VmPklBinaryEncoder; import org.pkl.core.runtime.VmTyped; -import org.pkl.core.runtime.VmUtils; import org.pkl.core.stdlib.ExternalMethod1Node; import org.pkl.core.stdlib.PklConverter; import org.pkl.core.util.Nullable; @@ -64,8 +61,6 @@ public final class RendererNodes { } private static VmPklBinaryEncoder createRenderer(VmTyped self, MessageBufferPacker packer) { - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); - return new VmPklBinaryEncoder(packer, converter); + return new VmPklBinaryEncoder(packer, PklConverter.fromRenderer(self)); } } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java index 108f49c9..06b7b76e 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/protobuf/RendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -135,9 +135,8 @@ public final class RendererNodes { @TruffleBoundary private static ProtobufRenderer createRenderer(VmTyped self, StringBuilder builder) { var indent = (String) VmUtils.readMember(self, Identifier.INDENT); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - return new ProtobufRenderer(builder, indent, new PklConverter(converters)); + return new ProtobufRenderer(builder, indent, PklConverter.fromRenderer(self)); } private static final class ProtobufRenderer extends AbstractStringRenderer { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java index c9c796e7..41c62175 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/test/report/JUnitReport.java @@ -236,8 +236,7 @@ public final class JUnitReport implements TestReport { private static String renderXML(String indent, String version, VmDynamic value) { var builder = new StringBuilder(); - var converter = new PklConverter(VmMapping.empty()); - var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), converter); + var renderer = new Renderer(builder, indent, version, "", VmMapping.empty(), PklConverter.NOOP); renderer.renderDocument(value); return builder.toString(); } diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java index 9bc4390d..a82e2e42 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/xml/RendererNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -34,10 +34,13 @@ public final class RendererNodes { var rootElementName = (String) VmUtils.readMember(self, Identifier.ROOT_ELEMENT_NAME); var rootElementAttributes = (VmMapping) VmUtils.readMember(self, Identifier.ROOT_ELEMENT_ATTRIBUTES); - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - var converter = new PklConverter(converters); return new Renderer( - builder, indent, xmlVersion, rootElementName, rootElementAttributes, converter); + builder, + indent, + xmlVersion, + rootElementName, + rootElementAttributes, + PklConverter.fromRenderer(self)); } public abstract static class renderDocument extends ExternalMethod1Node { diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java index 153b3e57..b2db9b05 100644 --- a/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java @@ -1,5 +1,5 @@ /* - * Copyright © 2024-2025 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. @@ -65,7 +65,7 @@ public final class ParserNodes { } private Object doParse(VmTyped self, String text, String uri) { - var converter = createConverter(self); + var converter = PklConverter.fromParser(self); var load = createLoad(self, text, uri, converter); try { @@ -101,7 +101,7 @@ public final class ParserNodes { } private VmList doParseAll(VmTyped self, String text, String uri) { - var converter = createConverter(self); + var converter = PklConverter.fromParser(self); var load = createLoad(self, text, uri, converter); var builder = VmList.EMPTY.builder(); @@ -123,11 +123,6 @@ public final class ParserNodes { } } - private static PklConverter createConverter(VmTyped self) { - var converters = (VmMapping) VmUtils.readMember(self, Identifier.CONVERTERS); - return new PklConverter(converters); - } - private static int getMaxCollectionAliases(VmTyped self) { var max = (Long) VmUtils.readMember(self, Identifier.MAX_COLLECTION_ALIASES); return max.intValue(); // has Pkl type `Int32(isPositive)` diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl new file mode 100644 index 00000000..08c12a37 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl @@ -0,0 +1,19 @@ +class LineComment extends ConvertProperty { + text: String + + valuePrefix: String = "" + prefix: String + suffix: String = "" + + render = (prop, renderer) -> + Pair(prop.key, new RenderDirective { + text = + List( + valuePrefix, + (renderer as ValueRenderer).renderValue(prop.value), + prefix, + outer.text, + suffix, + ).join("") + }) +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl new file mode 100644 index 00000000..5b0a83cd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl @@ -0,0 +1,63 @@ +open class Prefix extends ConvertProperty { + prefix: String + + render = (prop, _) -> Pair(prefix + prop.key, prop.value) +} + +class CamelCase extends ConvertProperty { + render = (prop, _) -> + Pair( + prop.key.replaceAllMapped(Regex("[^A-Za-z0-9]+([A-Za-z0-9])"), (match) -> + match.groups[1].value.toUpperCase() + ), + prop.value, + ) +} + +class MultiplyValue extends ConvertProperty { + factor: Number + render = (prop, _) -> + Pair(prop.key, prop.value as Number * factor) +} + +class SubtractValue extends ConvertProperty { + difference: Number + render = (prop, _) -> + Pair(prop.key, prop.value as Number - difference) +} + +open class Foo { + no_converter: Int = 1 + + @Prefix { prefix = "foo_" } + prefixed_with_foo: Int = 2 + + @Prefix { prefix = "foo_" } + base_class_first: Int = 3 + + @MultiplyValue { factor = 2 } + transform_value: Int = 4 + + @SubtractValue { difference = 2 } + @MultiplyValue { factor = 3 } + @ConvertProperty { + render = (prop, _) -> Pair(prop.key, prop.value as Number + 4) + } + @MultiplyValue { factor = 3 } + @SubtractValue { difference = 11 } + in_order: Int = 5 +} + +class Bar extends Foo { + @CamelCase + base_class_first: Int = 3 +} + +output { + value = new Bar {} + renderer = new PcfRenderer { + convertPropertyTransformers { + [Prefix] { prefix = "foo_" } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl new file mode 100644 index 00000000..376b6cbc --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl @@ -0,0 +1,27 @@ +import "pkl:json" + +import ".../input-helper/api/annotationConverter.pkl" + +@json.Property { name = "FOO" } +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new JsonRenderer { + convertPropertyTransformers { + // NB: this renders https://json5.org format which is a superset of JSON that supports commas. + [annotationConverter.LineComment] { prefix = ", // " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl new file mode 100644 index 00000000..2383f555 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl @@ -0,0 +1,26 @@ +import "pkl:jsonnet" + +import ".../input-helper/api/annotationConverter.pkl" + +@jsonnet.Property { name = "FOO" } +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new jsonnet.Renderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = ", // " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl new file mode 100644 index 00000000..684eaa79 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl @@ -0,0 +1,23 @@ +import ".../input-helper/api/annotationConverter.pkl" + +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new PListRenderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = "" } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl new file mode 100644 index 00000000..8b74c4f2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl @@ -0,0 +1,23 @@ +import ".../input-helper/api/annotationConverter.pkl" + +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new PcfRenderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = " // "; valuePrefix = "= " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl new file mode 100644 index 00000000..e958b9ab --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl @@ -0,0 +1,23 @@ +import ".../input-helper/api/annotationConverter.pkl" + +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new PropertiesRenderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = "\n# " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl new file mode 100644 index 00000000..80cd4b78 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl @@ -0,0 +1,28 @@ +import "pkl:protobuf" + +import ".../input-helper/api/annotationConverter.pkl" + +class Comment extends Annotation + +@protobuf.Property { name = "FOO" } +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new protobuf.Renderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = " # " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl new file mode 100644 index 00000000..8bff7723 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl @@ -0,0 +1,26 @@ +import "pkl:xml" + +import ".../input-helper/api/annotationConverter.pkl" + +@xml.Property { name = "FOO" } +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" +} + +output { + renderer = new xml.Renderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = "" } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl new file mode 100644 index 00000000..a819a487 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl @@ -0,0 +1,41 @@ +import "pkl:yaml" + +import ".../input-helper/api/annotationConverter.pkl" + +class Tag extends ConvertProperty { + tag: String(startsWith("!")) + + render = (prop, renderer) -> + if (renderer is YamlRenderer) + Pair(prop.key, new RenderDirective { + text = " \(tag) \(renderer.renderValue(prop.value))" + }) + else + prop +} + +@yaml.Property { name = "FOO" } +foo: String = "a" + +@annotationConverter.LineComment { text = "bar" } +bar: String = "b" + +baz: Nested + +class Nested { + @annotationConverter.LineComment { text = "qux" } + qux: String = "c" + + quux: String = "c" + + @Tag { tag = "!!foo" } + quuux: String = "d" +} + +output { + renderer = new YamlRenderer { + convertPropertyTransformers { + [annotationConverter.LineComment] { prefix = " # "; valuePrefix = " " } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf new file mode 100644 index 00000000..0c6a6ca4 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf @@ -0,0 +1,5 @@ +no_converter = 1 +foo_prefixed_with_foo = 2 +fooBaseClassFirst = 3 +transform_value = 8 +in_order = 28 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5 b/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5 new file mode 100644 index 00000000..4fde5aed --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5 @@ -0,0 +1,8 @@ +{ + "FOO": "a", + "bar": "b", // bar, + "baz": { + "qux": "c", // qux, + "quux": "c" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet b/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet new file mode 100644 index 00000000..22f2dae8 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet @@ -0,0 +1,8 @@ +{ + FOO: 'a', + bar: 'b', // bar, + baz: { + qux: 'c', // qux, + quux: 'c', + }, +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist new file mode 100644 index 00000000..b8119bba --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist @@ -0,0 +1,17 @@ + + + + + foo + a + bar + b + baz + + qux + c + quux + c + + + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf new file mode 100644 index 00000000..cc6fe762 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf @@ -0,0 +1,6 @@ +foo = "a" +bar = "b" // bar +baz { + qux = "c" // qux + quux = "c" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/pklbinary1.msgpack.yaml b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pklbinary1.msgpack.yaml index 4a4a4616..443647a8 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/api/pklbinary1.msgpack.yaml +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/pklbinary1.msgpack.yaml @@ -172,6 +172,12 @@ - - 3 - {} + - + - 16 + - 'convertPropertyTransformers' + - + - 3 + - {} - - 16 - 'extension' diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties b/pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties new file mode 100644 index 00000000..e26792dd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties @@ -0,0 +1,6 @@ +foo = a +bar = b +# bar +baz.qux = c +# qux +baz.quux = c diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb b/pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb new file mode 100644 index 00000000..99a26043 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb @@ -0,0 +1,6 @@ +FOO: "a" +bar: "b" # bar +baz: { + qux: "c" # qux + quux: "c" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml b/pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml new file mode 100644 index 00000000..c52f0378 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml @@ -0,0 +1,9 @@ + + + a + b + + c + c + + diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml b/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml new file mode 100644 index 00000000..1bdf27fb --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml @@ -0,0 +1,6 @@ +FOO: a +bar: b # bar +baz: + qux: c # qux + quux: c + quuux: !!foo d diff --git a/stdlib/base.pkl b/stdlib/base.pkl index e7a6e27b..23dd37c3 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -20,12 +20,14 @@ @ModuleInfo { minPklVersion = "0.31.0" } module pkl.base -// math import used for doc comments +// json, math, and yaml imports used for doc comments +import "pkl:json" import "pkl:jsonnet" import "pkl:math" import "pkl:pklbinary" import "pkl:protobuf" import "pkl:xml" +import "pkl:yaml" /// The top type of the type hierarchy. /// @@ -351,11 +353,70 @@ abstract class BaseValueRenderer { /// both match path spec `server.timeout`, whereas path `server.timeout.millis` does not. converters: Mapping Any> + /// Customizations for [ConvertProperty] annotation behaviors. + /// + /// This property is consulted to transform [ConvertProperty] annotation values. + /// This can be used to customize or override the conversion behavior for a specific renderer. + /// If multiple entries match the annotation's class, the most specific class (according to class + /// hierarchy) wins. + /// + /// See [ConvertProperty] for detailed information. + @Since { version = "0.31.0" } + convertPropertyTransformers: Mapping> + /// The file extension associated with this output format, /// or [null] if this format does not have an extension. extension: String? } +/// Conversion to be applied to properties when rendered through [BaseValueRenderer]. +/// +/// During rendering, the annotation's [render] function is called. +/// The function must return a [Pair] of the converted property name and value. +/// +/// Multiple [ConvertProperty] annotations can apply per property, and the output of one +/// annotation's [render] function is used as input to the next. +/// Annotations are applied the order as declared on the property. +/// If the annotated property is overriding a parent property, the parent property's annotations are +/// applied first. +/// +/// These conversions can coexist with [BaseValueRenderer.converters], and applies first. +/// +/// These conversions only affect rendering of class properties. +/// Applying this to other types of members does not impact rendering. +/// +/// These conversions can be overriden with [BaseValueRenderer.convertPropertyTransformers]. +/// +/// Example: +/// +/// ``` +/// // convert duration to the number of seconds +/// @ConvertProperty { +/// render = (property, _) -> Pair(property.key, property.value.toUnit("s")) +/// } +/// timeout: Duration +/// ``` +/// +/// [ConvertProperty] can be subclassed to define re-usable property converters. +/// The conversion defined in the previous example can be rewritten as: +/// +/// ``` +/// class ConvertDuration extends ConvertProperty { +/// unit: DurationUnit +/// +/// render = (property: Pair, _) -> Pair(property.key, property.value.toUnit(unit)) +/// } +/// +/// @ConvertDuration { unit = "s" } +/// timeout: Duration +/// ``` +@Since { version = "0.31.0" } +open class ConvertProperty extends Annotation { + /// Function called by [BaseValueRenderer] types during rendering to transform property + /// names and values. + render: (Pair, BaseValueRenderer) -> Pair +} + /// Base class for rendering Pkl values in some textual output format. /// /// A renderer's output is guaranteed to be well-formed unless [RenderDirective] is part of the @@ -466,6 +527,16 @@ class PcfRenderDirective { } /// Renders values as JSON. +/// +/// The [json.Property] annotation can be used to change how a property name renders into JSON. +/// +/// Example: +/// ``` +/// import "pkl:json" +/// +/// @json.Property { name = "wing_span" } +/// wingSpan: Int +/// ``` class JsonRenderer extends ValueRenderer { extension = "json" @@ -486,6 +557,16 @@ class JsonRenderer extends ValueRenderer { /// Renders values as YAML. /// /// To render a YAML stream, set [isStream] to [true]. +/// +/// The [yaml.Property] annotation can be used to change how a property name renders into YAML. +/// +/// Example: +/// ``` +/// import "pkl:yaml" +/// +/// @yaml.Property { name = "wing_span" } +/// wingSpan: Int +/// ``` class YamlRenderer extends ValueRenderer { extension = "yaml" diff --git a/stdlib/json.pkl b/stdlib/json.pkl index afce87d3..4d76fcab 100644 --- a/stdlib/json.pkl +++ b/stdlib/json.pkl @@ -18,6 +18,16 @@ @ModuleInfo { minPklVersion = "0.31.0" } module pkl.json +/// Annotate properties of classes and modules with this class to override how a [JsonRenderer] +/// interprets a property's name. +@Since { version = "0.31.0" } +class Property extends ConvertProperty { + /// The new name to use for the annotated property when rendered by [JsonRenderer]. + name: String + + render = (prop, renderer) -> if (renderer is JsonRenderer) Pair(name, prop.value) else prop +} + /// A JSON parser. /// /// JSON values are mapped to Pkl values as follows: diff --git a/stdlib/jsonnet.pkl b/stdlib/jsonnet.pkl index 3a2fd6e6..7e418975 100644 --- a/stdlib/jsonnet.pkl +++ b/stdlib/jsonnet.pkl @@ -68,6 +68,16 @@ function ExtVar(_name: String): ExtVar = new { name = _name } /// } /// } /// ``` +/// +/// The [Property] annotation can be used to change how a property name renders into Jsonnet. +/// +/// Example: +/// ``` +/// import "pkl:jsonnet" +/// +/// @jsonnet.Property { name = "wing_span" } +/// wingSpan: Int +/// ``` class Renderer extends ValueRenderer { extension = "jsonnet" @@ -91,6 +101,16 @@ class Renderer extends ValueRenderer { external function renderValue(value: Any): String } +/// Annotate properties of classes and modules with this class to override how a [Renderer] +/// interprets a property's name. +@Since { version = "0.31.0" } +class Property extends ConvertProperty { + /// The new name to use for the annotated property when rendered by [Renderer]. + name: String + + render = (prop, renderer) -> if (renderer is Renderer) Pair(name, prop.value) else prop +} + /// An `importstr` construct that, when evaluated by Jsonnet, returns the content of a UTF-8 text file. /// /// To construct an [ImportStr], use method [ImportStr()]. diff --git a/stdlib/pklbinary.pkl b/stdlib/pklbinary.pkl index f6c390b5..4fee00fb 100644 --- a/stdlib/pklbinary.pkl +++ b/stdlib/pklbinary.pkl @@ -32,6 +32,11 @@ module pkl.pklbinary /// Render values as `pkl-binary`. +/// +/// The `pkl-binary` renderer disables all [ConvertProperty] annotation converters by default +/// because `pkl-binary` data is intended to closely represent native Pkl types and data. +/// This behavior may be overridden for [ConvertProperty] or its subclasses by adding an entry to +/// [Renderer.convertPropertyTransformers]. class Renderer extends BytesRenderer { /// Renders [value] as `pkl-binary`. external function renderValue(value: Any): Bytes @@ -40,4 +45,11 @@ class Renderer extends BytesRenderer { /// /// Every `pkl-binary` value is also a valid document. external function renderDocument(value: Any): Bytes + + convertPropertyTransformers { + // disable all property conversions by default + [ConvertProperty] { + render = (property, _) -> property + } + } } diff --git a/stdlib/protobuf.pkl b/stdlib/protobuf.pkl index d1518bdb..c51bebda 100644 --- a/stdlib/protobuf.pkl +++ b/stdlib/protobuf.pkl @@ -25,6 +25,16 @@ import "pkl:reflect" /// Note: This class is _experimental_ and not ready for production use. /// /// As of this release, only Protocol Buffers' text format is supported. +/// +/// The [Property] annotation can be used to change how a property name renders into Protobuf. +/// +/// Example: +/// ``` +/// import "pkl:protobuf" +/// +/// @protobuf.Property { name = "wing_span" } +/// wingSpan: Int +/// ``` class Renderer extends ValueRenderer { /// The characters to use for indenting output. /// @@ -38,3 +48,13 @@ class Renderer extends ValueRenderer { /// Returns the canonical name for [type]. external function renderType(type: reflect.Type): String } + +/// Annotate properties of classes and modules with this class to override how a [Renderer] +/// interprets a property's name. +@Since { version = "0.31.0" } +class Property extends ConvertProperty { + /// The new name to use for the annotated property when rendered by [Renderer]. + name: String + + render = (prop, renderer) -> if (renderer is Renderer) Pair(name, prop.value) else prop +} diff --git a/stdlib/xml.pkl b/stdlib/xml.pkl index 99822ed8..3ec3ba4b 100644 --- a/stdlib/xml.pkl +++ b/stdlib/xml.pkl @@ -32,6 +32,16 @@ module pkl.xml /// /// To set the name and attributes of the XML document's root element, /// use [rootElementName] and [rootElementAttributes]. +/// +/// The [Property] annotation can be used to change how a property name renders into XML. +/// +/// Example: +/// ``` +/// import "pkl:xml" +/// +/// @xml.Property { name = "wing_span" } +/// wingSpan: Int +/// ``` class Renderer extends ValueRenderer { extension = "xml" @@ -52,6 +62,16 @@ class Renderer extends ValueRenderer { external function renderValue(value: Any): String } +/// Annotate properties of classes and modules with this class to override how a [Renderer] +/// interprets a property's name. +@Since { version = "0.31.0" } +class Property extends ConvertProperty { + /// The new name to use for the annotated property when rendered by [Renderer]. + name: String + + render = (prop, renderer) -> if (renderer is Renderer) Pair(name, prop.value) else prop +} + /// Creates an XML element with the given name. /// /// Use this method to directly define an XML element @@ -68,10 +88,10 @@ class Renderer extends ValueRenderer { /// /// To define the XML element's content, add child values (normally also called "elements") to the `Element` object: /// ``` -/// order = xml.Element("order") { // element with one child -/// xml.Element("item") { // element with two children -/// xml.Element("name") { "banana" } // element with one child -/// xml.Element("quantity") { 42 } // element with one child +/// order = (xml.Element("order")) { // element with one child +/// (xml.Element("item")) { // element with two children +/// (xml.Element("name")) { "banana" } // element with one child +/// (xml.Element("quantity")) { 42 } // element with one child /// } /// } /// ``` diff --git a/stdlib/yaml.pkl b/stdlib/yaml.pkl index 53bf8b51..4cfaaaa6 100644 --- a/stdlib/yaml.pkl +++ b/stdlib/yaml.pkl @@ -18,6 +18,16 @@ @ModuleInfo { minPklVersion = "0.31.0" } module pkl.yaml +/// Annotate properties of classes and modules with this class to override how a [YamlRenderer] +/// interprets a property's name. +@Since { version = "0.31.0" } +class Property extends ConvertProperty { + /// The new name to use for the annotated property when rendered by [YamlRenderer]. + name: String + + render = (prop, renderer) -> if (renderer is YamlRenderer) Pair(name, prop.value) else prop +} + /// A YAML parser. /// /// YAML values are mapped to Pkl values as follows: