mirror of
https://github.com/apple/pkl.git
synced 2026-02-25 11:54:57 +01:00
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:
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)`
|
||||
|
||||
19
pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl
vendored
Normal file
19
pkl-core/src/test/files/LanguageSnippetTests/input-helper/api/annotationConverter.pkl
vendored
Normal 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("")
|
||||
})
|
||||
}
|
||||
63
pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl
vendored
Normal file
63
pkl-core/src/test/files/LanguageSnippetTests/input/api/annotationConverters.pkl
vendored
Normal 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_" }
|
||||
}
|
||||
}
|
||||
}
|
||||
27
pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl
vendored
Normal file
27
pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonRenderer9.json5.pkl
vendored
Normal 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 = ", // " }
|
||||
}
|
||||
}
|
||||
}
|
||||
26
pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl
vendored
Normal file
26
pkl-core/src/test/files/LanguageSnippetTests/input/api/jsonnetRenderer8.jsonnet.pkl
vendored
Normal 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 = ", // " }
|
||||
}
|
||||
}
|
||||
}
|
||||
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl
vendored
Normal file
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/pListRenderer8.plist.pkl
vendored
Normal 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 = " -->" }
|
||||
}
|
||||
}
|
||||
}
|
||||
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl
vendored
Normal file
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/pcfRenderer9.pkl
vendored
Normal 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 = "= " }
|
||||
}
|
||||
}
|
||||
}
|
||||
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl
vendored
Normal file
23
pkl-core/src/test/files/LanguageSnippetTests/input/api/propertiesRenderer12.properties.pkl
vendored
Normal 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# " }
|
||||
}
|
||||
}
|
||||
}
|
||||
28
pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl
vendored
Normal file
28
pkl-core/src/test/files/LanguageSnippetTests/input/api/protobuf3.txtpb.pkl
vendored
Normal 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 = " # " }
|
||||
}
|
||||
}
|
||||
}
|
||||
26
pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl
vendored
Normal file
26
pkl-core/src/test/files/LanguageSnippetTests/input/api/xmlRenderer9.xml.pkl
vendored
Normal 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 = " -->" }
|
||||
}
|
||||
}
|
||||
}
|
||||
41
pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl
vendored
Normal file
41
pkl-core/src/test/files/LanguageSnippetTests/input/api/yamlRenderer10.yml.pkl
vendored
Normal 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 = " " }
|
||||
}
|
||||
}
|
||||
}
|
||||
5
pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf
vendored
Normal file
5
pkl-core/src/test/files/LanguageSnippetTests/output/api/annotationConverters.pcf
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
no_converter = 1
|
||||
foo_prefixed_with_foo = 2
|
||||
fooBaseClassFirst = 3
|
||||
transform_value = 8
|
||||
in_order = 28
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonRenderer9.json5
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"FOO": "a",
|
||||
"bar": "b", // bar,
|
||||
"baz": {
|
||||
"qux": "c", // qux,
|
||||
"quux": "c"
|
||||
}
|
||||
}
|
||||
8
pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet
vendored
Normal file
8
pkl-core/src/test/files/LanguageSnippetTests/output/api/jsonnetRenderer8.jsonnet
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
FOO: 'a',
|
||||
bar: 'b', // bar,
|
||||
baz: {
|
||||
qux: 'c', // qux,
|
||||
quux: 'c',
|
||||
},
|
||||
}
|
||||
17
pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist
vendored
Normal file
17
pkl-core/src/test/files/LanguageSnippetTests/output/api/pListRenderer8.plist
vendored
Normal 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>
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/pcfRenderer9.pcf
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
foo = "a"
|
||||
bar = "b" // bar
|
||||
baz {
|
||||
qux = "c" // qux
|
||||
quux = "c"
|
||||
}
|
||||
@@ -172,6 +172,12 @@
|
||||
-
|
||||
- 3
|
||||
- {}
|
||||
-
|
||||
- 16
|
||||
- 'convertPropertyTransformers'
|
||||
-
|
||||
- 3
|
||||
- {}
|
||||
-
|
||||
- 16
|
||||
- 'extension'
|
||||
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/propertiesRenderer12.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
foo = a
|
||||
bar = b
|
||||
# bar
|
||||
baz.qux = c
|
||||
# qux
|
||||
baz.quux = c
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/protobuf3.txtpb
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
FOO: "a"
|
||||
bar: "b" # bar
|
||||
baz: {
|
||||
qux: "c" # qux
|
||||
quux: "c"
|
||||
}
|
||||
9
pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml
vendored
Normal file
9
pkl-core/src/test/files/LanguageSnippetTests/output/api/xmlRenderer9.xml
vendored
Normal 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>
|
||||
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml
vendored
Normal file
6
pkl-core/src/test/files/LanguageSnippetTests/output/api/yamlRenderer10.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
FOO: a
|
||||
bar: b # bar
|
||||
baz:
|
||||
qux: c # qux
|
||||
quux: c
|
||||
quuux: !!foo d
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()].
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user