From 4f48546fcb705992382a07e9d9f646a0d429c705 Mon Sep 17 00:00:00 2001 From: Madmegsox1 Date: Wed, 14 Feb 2024 01:04:32 +0000 Subject: [PATCH 1/3] [Addition] Added IniRenderer and IniRenderer use cases #137 --- .../main/java/org/pkl/core/IniRenderer.java | 247 ++++++++++++++++++ .../java/org/pkl/core/ValueRenderers.java | 11 + .../java/org/pkl/core/util/ini/IniUtils.java | 65 +++++ .../kotlin/org/pkl/core/IniRendererTest.kt | 54 ++++ .../org/pkl/core/iniRendererTest.ini | 42 +++ .../org/pkl/core/iniRendererTest.pkl | 66 +++++ 6 files changed, 485 insertions(+) create mode 100644 pkl-core/src/main/java/org/pkl/core/IniRenderer.java create mode 100644 pkl-core/src/main/java/org/pkl/core/util/ini/IniUtils.java create mode 100644 pkl-core/src/test/kotlin/org/pkl/core/IniRendererTest.kt create mode 100644 pkl-core/src/test/resources/org/pkl/core/iniRendererTest.ini create mode 100644 pkl-core/src/test/resources/org/pkl/core/iniRendererTest.pkl diff --git a/pkl-core/src/main/java/org/pkl/core/IniRenderer.java b/pkl-core/src/main/java/org/pkl/core/IniRenderer.java new file mode 100644 index 000000000..38e3f87d8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/IniRenderer.java @@ -0,0 +1,247 @@ +/** + * Copyright © 2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.pkl.core.util.Nullable; +import org.pkl.core.util.ini.IniUtils; + +// To instantiate this class, use ValueRenderers.properties(). +final class IniRenderer implements ValueRenderer { + + private final Writer writer; + private final boolean omitNullProperties; + private final boolean restrictCharset; + + public IniRenderer(Writer writer, boolean omitNullProperties, boolean restrictCharset) { + this.writer = writer; + this.omitNullProperties = omitNullProperties; + this.restrictCharset = restrictCharset; + } + + @Override + public void renderDocument(Object value) { + new Visitor().renderDocument(value); + } + + @Override + public void renderValue(Object value) { + new Visitor().renderValue(value); + } + + public class Visitor implements ValueConverter { + + public void renderValue(Object value) { + write(convert(value), false, restrictCharset); + } + + public void renderDocument(Object value) { + if (value instanceof Composite) { + doVisitMap(null, ((Composite) value).getProperties()); + } else if (value instanceof Map) { + doVisitMap(null, (Map) value); + } else if (value instanceof Pair) { + Pair pair = (Pair) value; + doVisitKeyAndValue(null, pair.getFirst(), pair.getSecond()); + } else { + throw new RendererException( + String.format( + "The top-level value of a Java properties file must have type `Composite`, `Map`, or `Pair`, but got type `%s`.", + value.getClass().getTypeName())); + } + } + + private void doVisitMap(@Nullable String keyPrefix, Map map) { + for (Map.Entry entry : map.entrySet()) { + doVisitKeyAndValue(keyPrefix, entry.getKey(), entry.getValue()); + } + } + + private void doVisitKeyAndValue(@Nullable String keyPrefix, Object key, Object value) { + if (omitNullProperties && value instanceof PNull) { + return; + } + var baseKey = convert(key); + // gets existing key and appends the existing keypreifx if it's not null + var keyString = keyPrefix == null ? baseKey : keyPrefix + "." + baseKey; + // discovered a new section and writes key then writes the child values + // e.g. [example.dog] + if (value instanceof Composite) { + writeIniKey(keyString); + doVisitMap(keyString, ((Composite) value).getProperties()); + } else if (value instanceof Map) { + writeIniKey(keyString); + doVisitMap(keyString, (Map) value); + } else { + writeIniValue(baseKey, convert(value)); + } + } + + @Override + public String convertNull() { + return ""; + } + + @Override + public String convertString(String value) { + return value; + } + + @Override + public String convertBoolean(Boolean value) { + return value.toString(); + } + + @Override + public String convertInt(Long value) { + return value.toString(); + } + + @Override + public String convertFloat(Double value) { + return value.toString(); + } + + @Override + public String convertDuration(Duration value) { + throw new RendererException( + String.format( + "Values of type `Duration` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertDataSize(DataSize value) { + throw new RendererException( + String.format( + "Values of type `DateSize` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertPair(Pair value) { + throw new RendererException( + String.format( + "Values of type `Pair` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertList(List value) { + throw new RendererException( + String.format( + "Values of type `List` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertSet(Set value) { + throw new RendererException( + String.format("Values of type `Set` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertMap(Map value) { + throw new RendererException( + String.format("Values of type `Map` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertObject(PObject value) { + throw new RendererException( + String.format( + "Values of type `Object` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertModule(PModule value) { + throw new RendererException( + String.format( + "Values of type `Module` cannot be rendered as Properties. Value: %s", value)); + } + + @Override + public String convertClass(PClass value) { + throw new RendererException( + String.format( + "Values of type `Class` cannot be rendered as Properties. Value: %s", + value.getSimpleName())); + } + + @Override + public String convertTypeAlias(TypeAlias value) { + throw new RendererException( + String.format( + "Values of type `TypeAlias` cannot be rendered as Properties. Value: %s", + value.getSimpleName())); + } + + @Override + public String convertRegex(Pattern value) { + throw new RendererException( + String.format( + "Values of type `Regex` cannot be rendered as Properties. Value: %s", value)); + } + + private void write(String value, boolean escapeSpace, boolean restrictCharset) { + try { + writer.write(IniUtils.renderPropertiesKeyOrValue(value, escapeSpace, restrictCharset)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeIniValue(String key, String value) { + write(key, true, restrictCharset); + writeSeparator(); + write(value, false, restrictCharset); + writeLineBreak(); + } + + private void writeIniKey(String keyValue) { + try { + // inserts a line break to make sure ini file is correct format + writeLineBreak(); + writer.write('['); + write(keyValue, true, restrictCharset); + writer.write(']'); + writeLineBreak(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void writeSeparator() { + try { + writer.write(' '); + writer.write('='); + writer.write(' '); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeLineBreak() { + try { + writer.write('\n'); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java b/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java index 4e17202e6..0819fef2f 100644 --- a/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java +++ b/pkl-core/src/main/java/org/pkl/core/ValueRenderers.java @@ -67,4 +67,15 @@ public static ValueRenderer properties( Writer writer, boolean omitNullProperties, boolean restrictCharset) { return new PropertiesRenderer(writer, omitNullProperties, restrictCharset); } + + /** + * Creates a renderer for INI file format. If {@code omitNullProperties} is {@code true}, object + * properties and map entries whose value is {@code null} will not be rendered. If {@code + * restrictCharset} is {@code true} characters outside the printable US-ASCII charset range will + * be rendered as Unicode escapes + */ + public static ValueRenderer ini( + Writer writer, boolean omitNullProperties, boolean restrictCharset) { + return new IniRenderer(writer, omitNullProperties, restrictCharset); + } } diff --git a/pkl-core/src/main/java/org/pkl/core/util/ini/IniUtils.java b/pkl-core/src/main/java/org/pkl/core/util/ini/IniUtils.java new file mode 100644 index 000000000..3ac670ed2 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/util/ini/IniUtils.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.util.ini; + +public class IniUtils { + // bitmap 32 bit need to escape ' ', ';' + private static final int[] bitmapEscapeSpace = new int[] {9728, 738197900, 268435456, 0}; + + private static final int[] bitmapNoEscapeSpace = new int[] {9728, 738197644, 268435456, 0}; + + private static final char[] hexDigitTable = + new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + private static final char[] hashTable = + new char[] { + ' ', 0, '"', '#', 0, 0, 0, '\'', 0, 't', 'n', 0, 0, 'r', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ':', ';', '\\', '=', 0, 0 + }; + + public static String renderPropertiesKeyOrValue( + String value, boolean escapeSpace, boolean restrictCharset) { + if (value.isEmpty()) { + return ""; + } + var builder = new StringBuilder(); + + if (!escapeSpace && value.charAt(0) == ' ') { + builder.append('\\'); + } + + for (var i = 0; i < value.length(); i++) { + var c = value.charAt(i); + var bitmap = escapeSpace ? bitmapEscapeSpace : bitmapNoEscapeSpace; + var isEscapeChar = c < 128 && (bitmap[c >> 5] & (1 << c)) != 0; + + if (isEscapeChar) { + builder.append('\\').append(hashTable[c % 32]); + } else if (restrictCharset && (c < 32 || c > 126)) { + builder + .append('\\') + .append('u') + .append(hexDigitTable[c >> 12 & 0xF]) + .append(hexDigitTable[c >> 8 & 0xF]) + .append(hexDigitTable[c >> 4 & 0xF]) + .append(hexDigitTable[c & 0xF]); + } else { + builder.append(c); + } + } + return builder.toString(); + } +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/IniRendererTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/IniRendererTest.kt new file mode 100644 index 000000000..f8181efb6 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/IniRendererTest.kt @@ -0,0 +1,54 @@ +package org.pkl.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pkl.core.util.IoUtils +import java.io.StringWriter + +class IniRendererTest { + @Test + fun `render document`() { + val evaluator = Evaluator.preconfigured() + val module = evaluator.evaluate(ModuleSource.modulePath("org/pkl/core/iniRendererTest.pkl")) + val writer = StringWriter() + val renderer = ValueRenderers.ini(writer, true, false) + + renderer.renderDocument(module) + val output = writer.toString() + val expected = IoUtils.readClassPathResourceAsString(javaClass, "iniRendererTest.ini") + + + assertThat(output).isEqualTo(expected) + } + + @Test + fun `render unsupported document values`() { + val unsupportedValues = listOf( + "List()", "new Listing {}", "Map()", "new Mapping {}", "Set()", + "new PropertiesRenderer {}", "new Dynamic {}" + ) + + unsupportedValues.forEach { + val evaluator = Evaluator.preconfigured() + val renderer = ValueRenderers.ini(StringWriter(), true, false) + + val module = evaluator.evaluate(ModuleSource.text("value = $it")) + assertThrows { renderer.renderValue(module) } + } + } + + @Test + fun `rendered document ends in newline`() { + val module = Evaluator.preconfigured() + .evaluate(ModuleSource.text("foo { bar = 0 }")) + + for (omitNullProperties in listOf(false, true)) { + for (restrictCharSet in listOf(false, true)) { + val writer = StringWriter() + ValueRenderers.ini(writer, omitNullProperties, restrictCharSet).renderDocument(module) + assertThat(writer.toString()).endsWith("\n") + } + } + } +} diff --git a/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.ini b/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.ini new file mode 100644 index 000000000..21480325d --- /dev/null +++ b/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.ini @@ -0,0 +1,42 @@ +int = 123 +float = 1.23 +bool = true +string = Pigeon +unicodeString = abc😀abc😎abc +multiLineString = have a\ngreat\nday + +[map] +one = 123 +two = 1.23 +three = true +four = Pigeon +five = abc😀abc😎abc +six = have a\ngreat\nday + +[map.seven] +name = Pigeon + +[mapping] +one = 123 +two = 1.23 +three = true +four = Pigeon +five = abc😀abc😎abc +six = have a\ngreat\nday + +[mapping.seven] +name = Pigeon + +[typedObject] +name = Pigeon +age = 30 + +[typedObject.address] +street = Folsom St. + +[container] +name = Pigeon +age = 30 + +[container.address] +street = Folsom St. diff --git a/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.pkl b/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.pkl new file mode 100644 index 000000000..bd9a64e1a --- /dev/null +++ b/pkl-core/src/test/resources/org/pkl/core/iniRendererTest.pkl @@ -0,0 +1,66 @@ +class Person { + name: String + age: Int + address: Address + friend: Person? +} + +class Address { + street: String +} + +int = 123 + +float = 1.23 + +bool = true + +string = "Pigeon" + +unicodeString = "abc😀abc😎abc" + +multiLineString = """ + have a + great + day + """ + +map = Map( + "one", int, + "two", float, + "three", bool, + "four", string, + "five", unicodeString, + "six", multiLineString, + "seven", new Dynamic { name = "Pigeon" }, + "eight", null +) + +mapping = new Mapping { + ["one"] = int + ["two"] = float + ["three"] = bool + ["four"] = string + ["five"] = unicodeString + ["six"] = multiLineString + ["seven"] = new { name = "Pigeon" } + ["eight"] = null +} + +typedObject = new Person { + name = "Pigeon" + age = 30 + address { + street = "Folsom St." + } + friend = null +} + +container { + name = "Pigeon" + age = 30 + address { + street = "Folsom St." + } + friend = null +} From 9e8f5cc5cf9f25d10bee2c54ed598efe942bbead Mon Sep 17 00:00:00 2001 From: Madmegsox1 Date: Sun, 18 Feb 2024 15:37:27 +0000 Subject: [PATCH 2/3] [Addition] Added In-Language ini renderer, Tests for ini renderer. There are still some test's that need to be added. --- .../java/org/pkl/core/runtime/IniModule.java | 30 ++ .../org/pkl/core/runtime/ModuleCache.java | 2 + .../pkl/core/stdlib/ini/RendererNodes.java | 409 ++++++++++++++++++ .../input/api/iniRenderer1.ini.pkl | 35 ++ .../input/api/iniRenderer2.ini.pkl | 71 +++ .../output/api/iniRenderer1.ini | 10 + .../output/api/iniRenderer1.ini.pcf | 8 + .../output/api/iniRenderer2.ini | 58 +++ .../output/api/iniRenderer2.ini.pcf | 58 +++ .../output/errors/cannotFindStdLibModule.err | 1 + stdlib/base.pkl | 4 +- stdlib/ini.pkl | 66 +++ 12 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 pkl-core/src/main/java/org/pkl/core/runtime/IniModule.java create mode 100644 pkl-core/src/main/java/org/pkl/core/stdlib/ini/RendererNodes.java create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer1.ini.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer2.ini.pkl create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini.pcf create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini create mode 100644 pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini.pcf create mode 100644 stdlib/ini.pkl diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/IniModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/IniModule.java new file mode 100644 index 000000000..fd82ea15b --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/IniModule.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.runtime; + +import java.net.URI; + +public final class IniModule extends StdLibModule { + private static final VmTyped instance = VmUtils.createEmptyModule(); + + static { + loadModule(URI.create("pkl:ini"), instance); + } + + public static VmTyped getModule() { + return instance; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java index 4befb038b..8224dae40 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -111,6 +111,8 @@ public synchronized VmTyped getOrLoad( return TestModule.getModule(); case "xml": return XmlModule.getModule(); + case "ini": + return IniModule.getModule(); default: if (!STDLIB_MODULE_URIS.contains(moduleKey.getUri())) { var stdlibModules = String.join("\n", Release.current().standardLibrary().modules()); diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/ini/RendererNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/ini/RendererNodes.java new file mode 100644 index 000000000..8246badb1 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/ini/RendererNodes.java @@ -0,0 +1,409 @@ +/** + * Copyright © 2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pkl.core.stdlib.ini; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.dsl.Specialization; +import java.util.List; +import org.pkl.core.runtime.Identifier; +import org.pkl.core.runtime.VmDataSize; +import org.pkl.core.runtime.VmDuration; +import org.pkl.core.runtime.VmDynamic; +import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmIntSeq; +import org.pkl.core.runtime.VmList; +import org.pkl.core.runtime.VmListing; +import org.pkl.core.runtime.VmMap; +import org.pkl.core.runtime.VmMapping; +import org.pkl.core.runtime.VmNull; +import org.pkl.core.runtime.VmPair; +import org.pkl.core.runtime.VmRegex; +import org.pkl.core.runtime.VmSet; +import org.pkl.core.runtime.VmTyped; +import org.pkl.core.runtime.VmUtils; +import org.pkl.core.runtime.VmValueConverter; +import org.pkl.core.stdlib.AbstractRenderer; +import org.pkl.core.stdlib.ExternalMethod1Node; +import org.pkl.core.stdlib.PklConverter; +import org.pkl.core.util.MutableBoolean; +import org.pkl.core.util.ini.IniUtils; + +public final class RendererNodes { + + public abstract static class renderDocument extends ExternalMethod1Node { + + @Specialization + @TruffleBoundary + protected String eval(VmTyped self, Object value) { + var builder = new StringBuilder(); + createRenderer(self, builder).renderDocument(value); + if (builder.charAt(builder.length() - 1) != '\n') { + // writes break line at the end of the file to comply with ini styleing standards + builder.append('\n'); + } + return builder.toString(); + } + } + + public abstract static class renderValue extends ExternalMethod1Node { + + @Specialization + @TruffleBoundary + protected String eval(VmTyped self, Object value) { + var builder = new StringBuilder(); + createRenderer(self, builder).renderValue(value); + return builder.toString(); + } + } + + private static IniRenderer 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 IniRenderer(builder, omitNullProperties, restrictCharset, PklConverter); + } + + private static final class IniRenderer extends AbstractRenderer { + + private final boolean restrictCharset; + + private boolean isDocument; + + private String storedEntryKey; + private boolean storedIsFirst; + + public IniRenderer( + StringBuilder builder, + boolean omitNullProperties, + boolean restrictCharset, + PklConverter converter) { + super("ini", builder, "", converter, omitNullProperties, omitNullProperties); + this.restrictCharset = restrictCharset; + } + + @Override + public void visitString(String value) { + writeKeyOrValue(value); + } + + @Override + public void visitBoolean(Boolean value) { + writeKeyOrValue(value.toString()); + } + + @Override + public void visitInt(Long value) { + writeKeyOrValue(value.toString()); + } + + @Override + public void visitFloat(Double value) { + writeKeyOrValue(value.toString()); + } + + @Override + public void visitDuration(VmDuration value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitDataSize(VmDataSize value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitIntSeq(VmIntSeq value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitPair(VmPair value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitRegex(VmRegex value) { + cannotRenderTypeAddConverter(value); + } + + @Override + public void visitNull(VmNull value) { + if (isDocument) { + writeSeparator(); + writeLineBreak(); + } + } + + @Override + protected void visitDocument(Object value) { + if (!(value instanceof VmMap + || value instanceof VmTyped + || value instanceof VmMapping + || value instanceof VmDynamic)) { + throw new VmExceptionBuilder() + .evalError("invalidPropertiesTopLevelValue", VmUtils.getClass(value)) + .withProgramValue("Value", value) + .build(); + } + if (!VmUtils.isRenderDirective(value)) { + isDocument = true; + } + visit(value); + } + + @Override + protected void visitTopLevelValue(Object value) { + visit(value); + } + + @Override + protected void visitRenderDirective(VmTyped value) { + builder.append(VmUtils.readTextProperty(value)); + } + + @Override + protected void startDynamic(VmDynamic value) { + writeSection(); + } + + @Override + protected void startTyped(VmTyped value) { + writeSection(); + } + + @Override + protected void startListing(VmListing value) { + cannotRenderTypeAddConverter(value); + } + + @Override + protected void startMapping(VmMapping value) { + writeSection(); + } + + // throws error for now + @Override + protected void startList(VmList value) { + cannotRenderTypeAddConverter(value); + } + + // throws error for now + @Override + protected void startSet(VmSet value) { + cannotRenderTypeAddConverter(value); + } + + @Override + protected void startMap(VmMap value) { + writeSection(); + } + + // element of list so ignored for now + @Override + protected void visitElement(long index, Object value, boolean isFirst) {} + + @Override + protected void visitEntryKey(Object key, boolean isFirst) { + storedIsFirst = isFirst; + if (VmUtils.isRenderDirective(key)) { + visitRenderDirective((VmTyped) key); + writeSeparator(); + return; + } + + if (key instanceof String) { + storedEntryKey = (String) key; + return; + } + + cannotRenderNonStringKey(key); + } + + @Override + protected void visitEntryValue(Object value) { + if (!((value instanceof VmTyped) + || (value instanceof VmDynamic) + || (value instanceof VmMapping) + || (value instanceof VmMap))) { + if (!storedIsFirst) { + writeLineBreak(); + } + writeKeyOrValue(storedEntryKey); + writeSeparator(); + visit(value); + } + } + + @Override + protected void visitProperty(Identifier name, Object value, boolean isFirst) { + + if (!((value instanceof VmTyped) + || (value instanceof VmDynamic) + || (value instanceof VmMapping) + || (value instanceof VmMap))) { + if (!isFirst) { + writeLineBreak(); + } + writeKeyOrValue(name.toString()); + writeSeparator(); + visit(value); + } + } + + /* + These end functions group `VmTyped`, `VmMapping`, `VmDynamic`, `VmMap` at the end of the map and then writes them to builder + This is to comply with ini style standards. Please look at tests for example's + */ + @Override + protected void endDynamic(VmDynamic value, boolean isEmpty) { + value.forceAndIterateMemberValues( + ((memberKey, member, memberValue) -> { + if ((memberValue instanceof VmTyped) + || (memberValue instanceof VmDynamic) + || (memberValue instanceof VmMapping) + || (memberValue instanceof VmMap)) { + + if (memberKey instanceof Identifier) { + currPath.push(memberKey); + } else { + currPath.push(converter.convert(memberKey, List.of())); + } + visit(memberValue); + currPath.pop(); + } + return true; + })); + } + + @Override + protected void endTyped(VmTyped value, boolean isEmpty) { + value.forceAndIterateMemberValues( + ((memberKey, member, memberValue) -> { + if ((memberValue instanceof VmTyped) + || (memberValue instanceof VmDynamic) + || (memberValue instanceof VmMapping) + || (memberValue instanceof VmMap)) { + + if (memberKey instanceof Identifier) { + currPath.push(memberKey); + } else { + currPath.push(converter.convert(memberKey, List.of())); + } + visit(memberValue); + currPath.pop(); + } + return true; + })); + } + + // ignored for now + @Override + protected void endListing(VmListing value, boolean isEmpty) {} + + @Override + protected void endMapping(VmMapping value, boolean isEmpty) { + value.forceAndIterateMemberValues( + ((memberKey, member, memberValue) -> { + if ((memberValue instanceof VmTyped) + || (memberValue instanceof VmDynamic) + || (memberValue instanceof VmMapping) + || (memberValue instanceof VmMap)) { + + currPath.push(converter.convert(memberKey, List.of())); + visit(memberValue); + currPath.pop(); + } + return true; + })); + } + + // ignored for now + @Override + protected void endList(VmList value) {} + + // ignored for now + @Override + protected void endSet(VmSet value) {} + + @Override + protected void endMap(VmMap value) { + value.forEach( + (key) -> { + var memberKey = key.getKey(); + var memberValue = key.getValue(); + if ((memberValue instanceof VmTyped) + || (memberValue instanceof VmDynamic) + || (memberValue instanceof VmMapping) + || (memberValue instanceof VmMap)) { + currPath.push(converter.convert(memberKey, List.of())); + visit(memberValue); + currPath.pop(); + } + }); + } + + private void writeKeyOrValue(String value) { + builder.append(IniUtils.renderPropertiesKeyOrValue(value, false, restrictCharset)); + } + + private void writeSeparator() { + builder.append(" = "); + } + + private void writeLineBreak() { + builder.append("\n"); + } + + private String getSection() { + var sectionBuilder = new StringBuilder(); + var isFollowing = new MutableBoolean(false); + currPath + .descendingIterator() + .forEachRemaining( + path -> { + if (path == VmValueConverter.TOP_LEVEL_VALUE) { + return; + } + if (isFollowing.get()) { + sectionBuilder.append('.'); + } + if (VmUtils.isRenderDirective(path)) { + sectionBuilder.append(VmUtils.readTextProperty(path)); + } else { + sectionBuilder.append( + IniUtils.renderPropertiesKeyOrValue(path.toString(), true, restrictCharset)); + } + isFollowing.set(true); + }); + + return sectionBuilder.toString(); + } + + private void writeSection() { + if (!currPath.isEmpty() && currPath.getFirst() != VmValueConverter.TOP_LEVEL_VALUE) { + if (builder.charAt(builder.length() - 1) != '\n') { + writeLineBreak(); + } + writeLineBreak(); // writes break line to comply with ini styling standards + builder.append('['); + builder.append(getSection()); + builder.append(']'); + writeLineBreak(); + } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer1.ini.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer1.ini.pkl new file mode 100644 index 000000000..5798035cd --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer1.ini.pkl @@ -0,0 +1,35 @@ +import "pkl:ini" + +class Person { + name: String + age: Int + address: Address +} + +class Address { + street: String +} + +key = "value" + +unicodeString = "abc😀abc😎abc" + +nullValue = null + +typedObject = new Person { + name = "Pigeon" + age = 19 + address { + street = "Fun Road" + } +} + +multiLineString = """ + have a + great + day + """ + +output { + renderer = new ini.Renderer {} +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer2.ini.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer2.ini.pkl new file mode 100644 index 000000000..7c94b404c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/iniRenderer2.ini.pkl @@ -0,0 +1,71 @@ +import "pkl:ini" + +class Person{ + name: String + address: Address + age: Int +} + +class Address{ + street: String +} + +key = "value" +unicodeString = "abc😀abc😎abc" +nullValue = null + + +mapping = new Mapping{ + ["one"] = "value1" + ["two"] = new Person {name = "Pigeon" age = 19 address { + street = "fun Street" + }} + ["three"] = "value3" + ["four"] = 1.2 + ["five"] = new Mapping { + ["six"] = 123 + } + ["seven"] = true +} + +map = Map( + "one", "value1", + "two", new Person {name = "Pigeon" age = 19 address { + street = "Fun Street" + }}, + "three", "value3", + "four", 1.2, + "five", Map("six", 123), + "seven", true +) + +typedObject = new Person{ + name = "Pigeon" + address { + street = "Fun Street" + } + age = 19 +} + +dynamic = new Dynamic{ + one = "value1" + two = new Person {name = "Pigeon" age = 19 address { + street = "fun Street" + }} + three = "value3" + four = 1.2 + five = new Dynamic { + six = 123 + } + seven = true +} + +multiLineString = """ + have a + great + day + """ + +output { + renderer = new ini.Renderer {} +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini new file mode 100644 index 000000000..162b1aed3 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini @@ -0,0 +1,10 @@ +key = value +unicodeString = abc😀abc😎abc +multiLineString = have a\ngreat\nday + +[typedObject] +name = Pigeon +age = 19 + +[typedObject.address] +street = Fun Road diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini.pcf new file mode 100644 index 000000000..408bdcd10 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer1.ini.pcf @@ -0,0 +1,8 @@ +value = key + +[typedObject] +name = Pigeon +age = 19 + +[typedObject.address] +street = Fun Road diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini new file mode 100644 index 000000000..4b761439f --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini @@ -0,0 +1,58 @@ +key = value +unicodeString = abc😀abc😎abc +multiLineString = have a\ngreat\nday + +[mapping] +one = value1 +three = value3 +four = 1.2 +seven = true + +[mapping.two] +name = Pigeon +age = 19 + +[mapping.two.address] +street = fun Street + +[mapping.five] +six = 123 + +[map] +one = value1 +three = value3 +four = 1.2 +seven = true + +[map.two] +name = Pigeon +age = 19 + +[map.two.address] +street = Fun Street + +[map.five] +six = 123 + +[typedObject] +name = Pigeon +age = 19 + +[typedObject.address] +street = Fun Street + +[dynamic] +one = value1 +three = value3 +four = 1.2 +seven = true + +[dynamic.two] +name = Pigeon +age = 19 + +[dynamic.two.address] +street = fun Street + +[dynamic.five] +six = 123 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini.pcf new file mode 100644 index 000000000..809aad6ef --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/iniRenderer2.ini.pcf @@ -0,0 +1,58 @@ +value = key +value2 = key2 +value3 = key3 + +[mapping] +one = value1 +three = value3 +four = 1.2 +seven = true + +[mapping.two] +name = Pigeon +age = 19 + +[mapping.two.address] +street = fun Street + +[mapping.five] +six = 123 + +[map] +one = value1 +three = value3 +four = 1.2 +seven = true + +[map.two] +name = Pigeon +age = 19 + +[map.two.address] +street = Fun Street + +[map.five] +six = 123 + +[typedObject] +name = Pigeon +age = 19 + +[typedObject.address] +street = Fun Street + +[dynamic] +one = value1 +three = value3 +four = 1.2 +seven = true + +[dynamic.two] +name = Pigeon +age = 19 + +[dynamic.two.address] +street = fun Street + +[dynamic.five] +six = 123 diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err index 00e174c88..bc024917c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err @@ -10,6 +10,7 @@ pkl:base pkl:Benchmark pkl:DocPackageInfo pkl:DocsiteInfo +pkl:ini pkl:json pkl:jsonnet pkl:math diff --git a/stdlib/base.pkl b/stdlib/base.pkl index e30935885..7c98feed2 100644 --- a/stdlib/base.pkl +++ b/stdlib/base.pkl @@ -22,6 +22,7 @@ module pkl.base import "pkl:jsonnet" import "pkl:xml" +import "pkl:ini" import "pkl:protobuf" /// The top type of the type hierarchy. @@ -102,7 +103,8 @@ abstract external class Module { else if (format == "textproto") new protobuf.Renderer {} else if (format == "xml") new xml.Renderer {} else if (format == "yaml") new YamlRenderer {} - else throw("Unknown output format: `\(format)`. Supported formats are `json`, `jsonnet`, `pcf`, `plist`, `properties`, `textproto`, `xml`, `yaml`.") + else if (format == "ini") new ini.Renderer {} + else throw("Unknown output format: `\(format)`. Supported formats are `json`, `jsonnet`, `pcf`, `plist`, `properties`, `textproto`, `xml`, `yaml`, `ini`.") text = renderer.renderDocument(value) } } diff --git a/stdlib/ini.pkl b/stdlib/ini.pkl new file mode 100644 index 000000000..ba4c2a0c4 --- /dev/null +++ b/stdlib/ini.pkl @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 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. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// An ini renderer. +@ModuleInfo { minPklVersion = "0.26.0" } +module pkl.ini + + +/// An INI Renderer +/// +/// Values are rendered as follows depending on there type: +/// - `Int`, `Float`, `Boolean`, `String`: INI key-value pair +/// - `Map`, `Mapping`, `Typed`, `Dynamic`: sequence of INI sections with INI key-value pairs +/// - `List`, `Set`, `Listing`: are Unsupported for the time being but will consist of commer seperated values or array style values. +/// +/// The element order of the INI sequences, sets, and mappings is maintained. +/// +/// Known Limitations: +/// - Unsupported Types: `Duration`, `DataSize`, `IntSeq`, `Pair`, `Regex`, `List`, `Set`, `Listing` +/// - Comments are not maintained. +class Renderer extends ValueRenderer { + extension = "ini" + + /// Whether to skip rendering keys whose value is [null]. + omitNullProperties: Boolean = true + + /// Whether to render characters outside the printable US-ASCII charset range as + /// [Unicode escapes](https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.3). + restrictCharset: Boolean = false + + // Commented out for now + // Whether to render `List`, `Set`, `Listing` and there style + // - comma: + // + // `list = element1, element2, element3` + // - bracket: + // + // `list[] = element1` + // + // `list[] = element2` + // + // `list[] = element3` + // + // - left or set to null (default): + // + // Wont render `List`, `Set`, `Listing` + // arrayStyle: ("comma"|"bracket")? = null + + + external function renderDocument(value: Any): String + + external function renderValue(value: Any): String +} From 9c91fd481ddba88a0e399cdadab98e89bfc758a9 Mon Sep 17 00:00:00 2001 From: Madmegsox1 <68086870+Madmegsox1@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:56:45 +0000 Subject: [PATCH 3/3] Update pkl-core/src/main/java/org/pkl/core/IniRenderer.java As per @bioball suggestion Co-authored-by: Daniel Chao --- pkl-core/src/main/java/org/pkl/core/IniRenderer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkl-core/src/main/java/org/pkl/core/IniRenderer.java b/pkl-core/src/main/java/org/pkl/core/IniRenderer.java index 38e3f87d8..bbe84fe50 100644 --- a/pkl-core/src/main/java/org/pkl/core/IniRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/IniRenderer.java @@ -59,9 +59,6 @@ public void renderDocument(Object value) { doVisitMap(null, ((Composite) value).getProperties()); } else if (value instanceof Map) { doVisitMap(null, (Map) value); - } else if (value instanceof Pair) { - Pair pair = (Pair) value; - doVisitKeyAndValue(null, pair.getFirst(), pair.getSecond()); } else { throw new RendererException( String.format(