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
This commit is contained in:
Jen Basch
2026-01-23 12:44:41 -08:00
committed by GitHub
parent ed0cad668f
commit 73264e8fd1
51 changed files with 773 additions and 141 deletions

View File

@@ -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<VmTyped> getAllAnnotations(boolean ascending) {
var annotations = new ArrayList<VmTyped>();
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<VmTyped> 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<VmTyped> getAllAnnotations() {
var annotations = new ArrayList<VmTyped>();
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() {

View File

@@ -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");
}

View File

@@ -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<Identifier> {
// 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<Identifier> {
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<Identifier> {
// 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");

View File

@@ -37,7 +37,7 @@ public final class MirrorFactories {
public static final VmObjectFactory<VmTypeAlias> typeAliasFactory =
new VmObjectFactory<>(ReflectModule::getTypeAliasClass);
public static final VmObjectFactory<ClassProperty.Mirror> propertyFactory =
public static final VmObjectFactory<ClassProperty> propertyFactory =
new VmObjectFactory<>(ReflectModule::getPropertyClass);
public static final VmObjectFactory<ClassMethod> 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(

View File

@@ -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":

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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<T> {
Object WILDCARD_PROPERTY =
new Object() {
@@ -82,6 +85,9 @@ public interface VmValueConverter<T> {
T convertFunction(VmFunction value, Iterable<Object> path);
/** Returns with an empty identifier if the second value is a RenderDirective */
Pair<Identifier, T> convertProperty(ClassProperty property, Object value, Iterable<Object> path);
default T convert(Object value, Iterable<Object> path) {
if (value instanceof VmValue vmValue) {
return vmValue.accept(this, path);

View File

@@ -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());

View File

@@ -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<Object> {
private final Map<VmClass, VmFunction> typeConverters;
private final Map<VmClass, VmFunction> convertPropertyTransformers;
private final Pair<Object[], VmFunction>[] 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<Object> {
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<Object> {
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<Object> path) {
return doConvert(value, path, stringConverter);
@@ -177,6 +199,34 @@ public final class PklConverter implements VmValueConverter<Object> {
return doConvert(value, path, nullConverter);
}
@Override
public Pair<Identifier, Object> convertProperty(
ClassProperty property, Object value, Iterable<Object> 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<VmClass, VmFunction> createTypeConverters(VmMapping converters) {
var result = new HashMap<VmClass, VmFunction>();
converters.iterateMemberValues(
@@ -190,6 +240,19 @@ public final class PklConverter implements VmValueConverter<Object> {
return result;
}
private Map<VmClass, VmFunction> createConvertPropertyTransformers(
VmMapping convertPropertyTransformers) {
var result = new HashMap<VmClass, VmFunction>();
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<Object[], VmFunction>[] createPathConverters(VmMapping converters) {
var result = new ArrayList<Pair<Object[], VmFunction>>();
@@ -221,8 +284,16 @@ public final class PklConverter implements VmValueConverter<Object> {
* 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 <T> @Nullable T findConverterByType(Map<VmClass, T> 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;

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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<Object, ObjectMember>, EconomicMap<Object, ObjectMember>> {
private final PklConverter converter;

View File

@@ -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 {

View File

@@ -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));
}
}

View File

@@ -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 {

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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)`

View File

@@ -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("")
})
}

View File

@@ -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_" }
}
}
}

View File

@@ -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 = ", // " }
}
}
}

View File

@@ -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 = ", // " }
}
}
}

View File

@@ -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 = "<!-- "; suffix = " -->" }
}
}
}

View File

@@ -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 = "= " }
}
}
}

View File

@@ -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# " }
}
}
}

View File

@@ -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 = " # " }
}
}
}

View File

@@ -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 = "<!-- "; suffix = " -->" }
}
}
}

View File

@@ -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 = " " }
}
}
}

View File

@@ -0,0 +1,5 @@
no_converter = 1
foo_prefixed_with_foo = 2
fooBaseClassFirst = 3
transform_value = 8
in_order = 28

View File

@@ -0,0 +1,8 @@
{
"FOO": "a",
"bar": "b", // bar,
"baz": {
"qux": "c", // qux,
"quux": "c"
}
}

View File

@@ -0,0 +1,8 @@
{
FOO: 'a',
bar: 'b', // bar,
baz: {
qux: 'c', // qux,
quux: 'c',
},
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>foo</key>
<string>a</string>
<key>bar</key>
<string>b</string><!-- bar -->
<key>baz</key>
<dict>
<key>qux</key>
<string>c</string><!-- qux -->
<key>quux</key>
<string>c</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,6 @@
foo = "a"
bar = "b" // bar
baz {
qux = "c" // qux
quux = "c"
}

View File

@@ -172,6 +172,12 @@
-
- 3
- {}
-
- 16
- 'convertPropertyTransformers'
-
- 3
- {}
-
- 16
- 'extension'

View File

@@ -0,0 +1,6 @@
foo = a
bar = b
# bar
baz.qux = c
# qux
baz.quux = c

View File

@@ -0,0 +1,6 @@
FOO: "a"
bar: "b" # bar
baz: {
qux: "c" # qux
quux: "c"
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<FOO>a</FOO>
<bar>b<!-- bar --></bar>
<baz>
<qux>c<!-- qux --></qux>
<quux>c</quux>
</baz>
</root>

View File

@@ -0,0 +1,6 @@
FOO: a
bar: b # bar
baz:
qux: c # qux
quux: c
quuux: !!foo d

View File

@@ -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<Class | String, (unknown) -> 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<Class, Mixin<ConvertProperty>>
/// 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<String, Duration>, _) -> 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<String, Any>, BaseValueRenderer) -> Pair<String, Any>
}
/// 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"

View File

@@ -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:

View File

@@ -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()].

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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
/// }
/// }
/// ```

View File

@@ -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: