();
9 |
10 | @Override
11 | public R onUnsignedInteger(BigInteger value) {
12 | return notExpected("Unsigned integer");
13 | }
14 |
15 | @Override
16 | public R onNegativeInteger(BigInteger value) {
17 | return notExpected("Negative integer");
18 | }
19 |
20 | @Override
21 | public R onByteString(byte[] value) {
22 | return notExpected("Byte string");
23 | }
24 |
25 | @Override
26 | public R onTextString(String value) {
27 | return notExpected("Text string");
28 | }
29 |
30 | @Override
31 | public R onVariableArray(BigInteger length, String name) {
32 | return notExpected("Variable array");
33 | }
34 |
35 | @Override
36 | public R onArray(BigInteger length, BigInteger tagI) {
37 | return notExpected("Array");
38 | }
39 |
40 | @Override
41 | public R onMap(BigInteger size) {
42 | return notExpected("Map");
43 | }
44 |
45 | @Override
46 | public R onFalse() {
47 | return notExpected("False");
48 | }
49 |
50 | @Override
51 | public R onTrue() {
52 | return notExpected("True");
53 | }
54 |
55 | @Override
56 | public R onNull() {
57 | return null;
58 | }
59 |
60 | @Override
61 | public R onHalfFloat(float value) {
62 | return notExpected("Half float");
63 | }
64 |
65 | @Override
66 | public R onSingleFloat(float value) {
67 | return notExpected("Single float");
68 | }
69 |
70 | @Override
71 | public R onDoubleFloat(double value) {
72 | return notExpected("Double float");
73 | }
74 |
75 | @Override
76 | public R onTag() {
77 | return null;
78 | }
79 |
80 | private R notExpected(String msg) {
81 | throw new CborException(msg + " not expected - expected null");
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/Shift.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.math.BigInteger;
4 | import java.net.URI;
5 | import java.nio.file.Path;
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 | import java.util.Iterator;
9 | import java.util.List;
10 | import java.util.Map.Entry;
11 | import org.dhallj.core.Expr;
12 | import org.dhallj.core.Operator;
13 | import org.dhallj.core.Source;
14 | import org.dhallj.core.Visitor;
15 |
16 | /**
17 | * Shifts all instances of a variable.
18 | *
19 | * Note that this visitor maintains internal state and instances should not be reused.
20 | */
21 | public final class Shift extends Visitor.Identity {
22 | private final int change;
23 | private final String name;
24 | private int cutoff = 0;
25 |
26 | public Shift(boolean isIncrement, String name) {
27 | this.change = isIncrement ? 1 : -1;
28 | this.name = name;
29 | }
30 |
31 | @Override
32 | public Expr onIdentifier(Expr self, String name, long index) {
33 | if (name.equals(this.name) && index >= this.cutoff) {
34 | return Expr.makeIdentifier(name, index + this.change);
35 | } else {
36 | return self;
37 | }
38 | }
39 |
40 | @Override
41 | public void bind(String name, Expr type) {
42 | if (name.equals(this.name)) {
43 | this.cutoff += 1;
44 | }
45 | }
46 |
47 | @Override
48 | public Expr onLambda(String name, Expr type, Expr result) {
49 | if (name.equals(this.name)) {
50 | this.cutoff -= 1;
51 | }
52 |
53 | return Expr.makeLambda(name, type, result);
54 | }
55 |
56 | @Override
57 | public Expr onPi(String name, Expr type, Expr result) {
58 | if (name.equals(this.name)) {
59 | this.cutoff -= 1;
60 | }
61 |
62 | return Expr.makePi(name, type, result);
63 | }
64 |
65 | @Override
66 | public Expr onLet(List> bindings, Expr body) {
67 | for (Expr.LetBinding binding : bindings) {
68 | if (binding.getName().equals(this.name)) {
69 | this.cutoff -= 1;
70 | }
71 | }
72 | return Expr.makeLet(bindings, body);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/modules/circe/src/main/scala/org/dhallj/circe/Converter.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.circe
2 |
3 | import io.circe.{Json, JsonNumber, JsonObject}
4 | import java.math.BigInteger
5 | import java.util.AbstractMap.SimpleImmutableEntry
6 | import java.util.Map.Entry
7 | import org.dhallj.core.Expr
8 | import org.dhallj.core.converters.JsonConverter
9 |
10 | object Converter {
11 | def apply(expr: Expr): Option[Json] = {
12 | val handler = new CirceHandler()
13 | val wasConverted = expr.accept(new JsonConverter(handler, false))
14 |
15 | if (wasConverted) Some(handler.result) else None
16 | }
17 |
18 | private[this] val folder: Json.Folder[Expr] = new Json.Folder[Expr] {
19 | def onBoolean(value: Boolean): Expr = if (value) Expr.Constants.TRUE else Expr.Constants.FALSE
20 | def onNull: Expr = Expr.makeApplication(Expr.Constants.NONE, Expr.Constants.EMPTY_RECORD_TYPE)
21 | def onNumber(value: JsonNumber): Expr = {
22 | val asDouble = value.toDouble
23 |
24 | if (java.lang.Double.compare(asDouble, -0.0) == 0) {
25 | Expr.makeDoubleLiteral(asDouble)
26 | } else
27 | value.toBigInt match {
28 | case Some(integer) =>
29 | if (integer.underlying.compareTo(BigInteger.ZERO) > 0) {
30 | Expr.makeNaturalLiteral(integer.underlying)
31 | } else {
32 | Expr.makeIntegerLiteral(integer.underlying)
33 | }
34 | case None => Expr.makeDoubleLiteral(asDouble)
35 | }
36 | }
37 | def onString(value: String): Expr = Expr.makeTextLiteral(value)
38 | def onObject(value: JsonObject): Expr = Expr.makeRecordLiteral(
39 | value.toMap.map { case (k, v) =>
40 | new SimpleImmutableEntry(k, v.foldWith(this)): Entry[String, Expr]
41 | }.toArray
42 | )
43 | def onArray(value: Vector[Json]): Expr =
44 | if (value.isEmpty) {
45 | Expr.makeEmptyListLiteral(Expr.makeApplication(Expr.Constants.LIST, Expr.Constants.EMPTY_RECORD_TYPE))
46 | } else {
47 | Expr.makeNonEmptyListLiteral(value.map(_.foldWith(this)).toArray)
48 | }
49 | }
50 |
51 | def apply(json: Json): Expr = json.foldWith(folder)
52 | }
53 |
--------------------------------------------------------------------------------
/modules/imports/src/test/scala/org/dhallj/imports/ReferentialSanityCheckSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports
2 |
3 | import java.net.URI
4 | import java.nio.file.Paths
5 |
6 | import cats.effect.IO
7 | import munit.FunSuite
8 | import org.dhallj.imports.ImportContext._
9 |
10 | class ReferentialSanityCheckSuite extends FunSuite {
11 |
12 | private val someUri = new URI("http://example.com/foo.dhall")
13 | private val somePath = Paths.get("/foo.dhall")
14 |
15 | test("Remote imports local".fail) {
16 | ReferentialSanityCheck[IO](Remote(someUri, null), Local(somePath)).unsafeRunSync()
17 | }
18 |
19 | test("Remote imports env".fail) {
20 | ReferentialSanityCheck[IO](Remote(someUri, null), Env("foo")).unsafeRunSync()
21 | }
22 |
23 | test("Remote imports remote") {
24 | ReferentialSanityCheck[IO](Remote(someUri, null), Remote(someUri, null)).unsafeRunSync()
25 | }
26 |
27 | test("Remote imports missing") {
28 | ReferentialSanityCheck[IO](Remote(someUri, null), Missing).unsafeRunSync()
29 | }
30 |
31 | test("Remote imports classpath".fail) {
32 | ReferentialSanityCheck[IO](Remote(someUri, null), Classpath(somePath)).unsafeRunSync()
33 | }
34 |
35 | test("Local imports local") {
36 | ReferentialSanityCheck[IO](Local(somePath), Local(somePath)).unsafeRunSync()
37 | }
38 |
39 | test("Local imports env") {
40 | ReferentialSanityCheck[IO](Local(somePath), Env("foo")).unsafeRunSync()
41 | }
42 |
43 | test("Local imports remote") {
44 | ReferentialSanityCheck[IO](Local(somePath), Remote(someUri, null)).unsafeRunSync()
45 | }
46 |
47 | test("Local imports missing") {
48 | ReferentialSanityCheck[IO](Local(somePath), Missing).unsafeRunSync()
49 | }
50 |
51 | test("Env imports local") {
52 | ReferentialSanityCheck[IO](Env("foo"), Local(somePath)).unsafeRunSync()
53 | }
54 |
55 | test("Env imports env") {
56 | ReferentialSanityCheck[IO](Env("foo"), Env("foo")).unsafeRunSync()
57 | }
58 |
59 | test("Env imports remote") {
60 | ReferentialSanityCheck[IO](Env("foo"), Remote(someUri, null)).unsafeRunSync()
61 | }
62 |
63 | test("Env imports missing") {
64 | ReferentialSanityCheck[IO](Env("foo"), Missing).unsafeRunSync()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeToMap.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.AbstractMap.SimpleImmutableEntry;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 | import java.util.Map.Entry;
7 | import org.dhallj.core.Expr;
8 |
9 | final class BetaNormalizeToMap {
10 | static final Expr apply(Expr base, Expr type) {
11 | List> baseAsRecordLiteral = Expr.Util.asRecordLiteral(base);
12 |
13 | if (baseAsRecordLiteral != null) {
14 | if (baseAsRecordLiteral.size() == 0) {
15 | return Expr.makeEmptyListLiteral(type);
16 | } else {
17 | Expr[] result = new Expr[baseAsRecordLiteral.size()];
18 |
19 | for (int i = 0; i < baseAsRecordLiteral.size(); i++) {
20 | Entry entry = baseAsRecordLiteral.get(i);
21 |
22 | result[i] = makeRecord(entry.getKey(), entry.getValue());
23 | }
24 |
25 | return Expr.makeNonEmptyListLiteral(result);
26 | }
27 | } else {
28 | return Expr.makeToMap(base, type);
29 | }
30 | }
31 |
32 | private static final String escape(String input) {
33 | StringBuilder builder = new StringBuilder();
34 |
35 | for (int i = 0; i < input.length(); i++) {
36 | char c = input.charAt(i);
37 |
38 | if (c == '\\') {
39 | char next = input.charAt(++i);
40 |
41 | if (next == '\\') {
42 | builder.append("\\\\");
43 | } else if (next == 'n') {
44 | builder.append("\\\\n");
45 | } else if (next == '$') {
46 | builder.append("\\\\\\$");
47 | } else {
48 | builder.append("\\\\");
49 | builder.append(next);
50 | }
51 | } else {
52 | builder.append(c);
53 | }
54 | }
55 |
56 | return builder.toString();
57 | }
58 |
59 | private static final Expr makeRecord(String key, Expr value) {
60 | Entry[] fields = {
61 | new SimpleImmutableEntry<>(
62 | Expr.Constants.MAP_KEY_FIELD_NAME, Expr.makeTextLiteral(escape(key))),
63 | new SimpleImmutableEntry<>(Expr.Constants.MAP_VALUE_FIELD_NAME, value)
64 | };
65 |
66 | return Expr.makeRecordLiteral(fields);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/Source.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core;
2 |
3 | /** Represents a section of a source document corresponding to a parsed expression. */
4 | public abstract class Source {
5 | final int beginLine;
6 | final int beginColumn;
7 | final int endLine;
8 | final int endColumn;
9 |
10 | public Source(int beginLine, int beginColumn, int endLine, int endColumn) {
11 | this.beginLine = beginLine;
12 | this.beginColumn = beginColumn;
13 | this.endLine = endLine;
14 | this.endColumn = endColumn;
15 | }
16 |
17 | public abstract void printText(StringBuilder builder);
18 |
19 | public final String getText() {
20 | StringBuilder builder = new StringBuilder();
21 | this.printText(builder);
22 | return builder.toString();
23 | }
24 |
25 | public final int getBeginLine() {
26 | return this.beginLine;
27 | }
28 |
29 | public final int getBeginColumn() {
30 | return this.beginColumn;
31 | }
32 |
33 | public final int getEndLine() {
34 | return this.endLine;
35 | }
36 |
37 | public final int getEndColumn() {
38 | return this.endColumn;
39 | }
40 |
41 | public final String toString() {
42 | StringBuilder builder = new StringBuilder("[(");
43 | builder.append(this.beginLine);
44 | builder.append(", ");
45 | builder.append(this.beginColumn);
46 | builder.append(") (");
47 | builder.append(this.endLine);
48 | builder.append(", ");
49 | builder.append(this.endColumn);
50 | builder.append(")]\n");
51 | this.printText(builder);
52 | return builder.toString();
53 | }
54 |
55 | private static final class FromString extends Source {
56 | private final String text;
57 |
58 | FromString(String text, int beginLine, int beginColumn, int endLine, int endColumn) {
59 | super(beginLine, beginColumn, endLine, endColumn);
60 | this.text = text;
61 | }
62 |
63 | public final void printText(StringBuilder builder) {
64 | builder.append(this.text);
65 | }
66 | }
67 |
68 | public static final Source fromString(
69 | String text, int beginLine, int beginColumn, int endLine, int endColumn) {
70 | return new FromString(text, beginLine, beginColumn, endLine, endColumn);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/modules/yaml/src/main/java/org/dhallj/yaml/YamlHandler.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.yaml;
2 |
3 | import java.math.BigInteger;
4 | import java.util.ArrayDeque;
5 | import java.util.Deque;
6 | import org.dhallj.core.converters.JsonHandler;
7 |
8 | public class YamlHandler implements JsonHandler {
9 | private final Deque stack = new ArrayDeque<>();
10 | private final boolean skipNulls;
11 |
12 | public YamlHandler(boolean skipNulls) {
13 | this.skipNulls = skipNulls;
14 | }
15 |
16 | public YamlHandler() {
17 | this(true);
18 | }
19 |
20 | private final void addValue(Object value) {
21 | if (stack.isEmpty()) {
22 | stack.push(new RootContext(value));
23 | } else {
24 | stack.peek().add(value);
25 | }
26 | }
27 |
28 | public Object getResult() {
29 | return this.stack.pop().getResult();
30 | }
31 |
32 | public void onNull() {
33 | this.addValue(null);
34 | }
35 |
36 | public void onBoolean(boolean value) {
37 | this.addValue(value);
38 | }
39 |
40 | public void onNumber(BigInteger value) {
41 | this.addValue(value);
42 | }
43 |
44 | public void onDouble(double value) {
45 | this.addValue(value);
46 | }
47 |
48 | public void onString(String value) {
49 | this.addValue(value.replaceAll("\\\\n", "\n").replaceAll("\\\\\"", "\""));
50 | }
51 |
52 | public void onArrayStart() {
53 | this.stack.push(new ArrayContext());
54 | }
55 |
56 | public void onArrayEnd() {
57 | YamlContext current = this.stack.pop();
58 |
59 | if (this.stack.isEmpty()) {
60 | this.stack.push(current);
61 | } else {
62 | this.stack.peek().add(current.getResult());
63 | }
64 | }
65 |
66 | public void onArrayElementGap() {}
67 |
68 | public void onObjectStart() {
69 | this.stack.push(new ObjectContext(this.skipNulls));
70 | }
71 |
72 | public void onObjectEnd() {
73 | YamlContext current = this.stack.pop();
74 |
75 | if (this.stack.isEmpty()) {
76 | this.stack.push(current);
77 | } else {
78 | this.stack.peek().add(current.getResult());
79 | }
80 | }
81 |
82 | public void onObjectField(String name) {
83 | this.stack.peek().add(name);
84 | }
85 |
86 | public void onObjectFieldGap() {}
87 | }
88 |
--------------------------------------------------------------------------------
/modules/jawn/src/main/scala/org/dhallj/jawn/FacadeHandler.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.jawn
2 |
3 | import java.math.BigInteger
4 | import java.util.{ArrayDeque, Deque}
5 | import org.dhallj.core.converters.JsonHandler
6 | import org.typelevel.jawn.{FContext, Facade}
7 |
8 | class FacadeHandler[J](facade: Facade[J]) extends JsonHandler {
9 | final protected[this] val stack: Deque[FContext[J]] = new ArrayDeque()
10 | final protected[this] val position: Int = 0
11 |
12 | protected[this] def addValue(value: J): Unit = {
13 | if (stack.isEmpty) {
14 | stack.push(facade.singleContext(position))
15 | }
16 | stack.peek.add(value, position)
17 | }
18 |
19 | final def result: J = stack.pop().finish(position)
20 |
21 | final def onNull(): Unit = addValue(facade.jnull(position))
22 | final def onBoolean(value: Boolean): Unit = addValue(if (value) facade.jtrue(position) else facade.jfalse(position))
23 | def onNumber(value: BigInteger): Unit = addValue(facade.jnum(value.toString(), -1, -1, position))
24 | def onDouble(value: Double): Unit =
25 | if (java.lang.Double.isFinite(value)) {
26 | val asString = java.lang.Double.toString(value)
27 |
28 | addValue(facade.jnum(asString, asString.indexOf('.'), asString.indexOf('E'), position))
29 | } else {
30 | addValue(facade.jnull(position))
31 | }
32 |
33 | def onString(value: String): Unit = addValue(facade.jstring(value, position))
34 |
35 | final def onArrayStart(): Unit = stack.push(facade.arrayContext(position))
36 |
37 | final def onArrayEnd(): Unit = {
38 | val current = stack.pop()
39 |
40 | if (stack.isEmpty) {
41 | stack.push(current)
42 | } else {
43 | stack.peek.add(current.finish(position), position)
44 | }
45 | }
46 |
47 | final def onArrayElementGap(): Unit = ()
48 |
49 | final def onObjectStart(): Unit = stack.push(facade.objectContext(position))
50 |
51 | final def onObjectEnd(): Unit = {
52 | val current = stack.pop()
53 |
54 | if (stack.isEmpty) {
55 | stack.push(current)
56 | } else {
57 | stack.peek.add(current.finish(position), position)
58 | }
59 | }
60 |
61 | final def onObjectField(name: String): Unit = stack.peek.add(name, position)
62 |
63 | final def onObjectFieldGap(): Unit = ()
64 | }
65 |
--------------------------------------------------------------------------------
/tests/src/test/scala/org/dhallj/tests/JsonConverterSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.tests
2 |
3 | import munit.FunSuite
4 | import org.dhallj.core.converters.JsonConverter
5 | import org.dhallj.parser.DhallParser
6 |
7 | class JsonConverterSuite extends FunSuite() {
8 | test("toCompactString correctly escapes text") {
9 | val expr = DhallParser.parse("""[{mapKey = " \n \$ \" ", mapValue = " \n \$ \" "}]""")
10 |
11 | assert(clue(JsonConverter.toCompactString(expr)) == clue("""{" \n $ \" ":" \n $ \" "}"""))
12 | }
13 |
14 | test("toCompactString correctly escapes text from Text/show") {
15 | val expr = DhallParser.parse("""Text/show "$100 #"""").normalize
16 |
17 | assert(clue(JsonConverter.toCompactString(expr)) == clue(""""\"\\u0024100 #\"""""))
18 | }
19 |
20 | test("toCompactString correctly escapes text from toMap") {
21 | val expr = DhallParser.parse("""toMap {` \n \$ \" ` = " \n \$ \" "}""").normalize
22 |
23 | assert(clue(JsonConverter.toCompactString(expr)) == clue("""{" \\n \\$ \\\" ":" \n $ \" "}"""))
24 | }
25 |
26 | test("toCompactString flattens toMap-formatted lists") {
27 | val expr = DhallParser.parse(
28 | """[{ mapKey = "foo", mapValue = 1}, {mapKey = "bar", mapValue = 2}]"""
29 | )
30 |
31 | assert(clue(JsonConverter.toCompactString(expr)) == """{"foo":1,"bar":2}""")
32 | }
33 |
34 | test("toCompactString flattens toMap-typed empty lists") {
35 | val expr = DhallParser.parse(
36 | """[]: List { mapKey: Text, mapValue: Integer }"""
37 | )
38 |
39 | assert(clue(JsonConverter.toCompactString(expr)) == """{}""")
40 | }
41 |
42 | test("toCompactString keeps last value in case of toMap-format duplicates") {
43 | val expr = DhallParser.parse(
44 | """[{ mapKey = "foo", mapValue = 100}, {mapKey = "foo", mapValue = 2 }]"""
45 | )
46 |
47 | assert(clue(JsonConverter.toCompactString(expr)) == """{"foo":2}""")
48 | }
49 |
50 | test("toCompactString doesn't flatten near-toMap-format lists") {
51 | val expr = DhallParser.parse(
52 | """[{ mapKey = "foo", mapValue = 1}, {mapKey = "bar", other = 1, mapValue = 2}]"""
53 | )
54 |
55 | val expected = """[{"mapKey":"foo","mapValue":1},{"mapKey":"bar","other":1,"mapValue":2}]"""
56 |
57 | assert(clue(JsonConverter.toCompactString(expr)) == clue(expected))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/cbor/HalfFloat.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.cbor;
2 |
3 | /**
4 | * Conversions between 16 and 32-bit floating point numbers.
5 | *
6 | * @see this Stack Overflow answer
7 | */
8 | public final class HalfFloat {
9 | public static final int fromFloat(float asFloat) {
10 | int bits = Float.floatToIntBits(asFloat);
11 | int sign = bits >>> 16 & 0x8000; // sign only
12 | int value = (bits & 0x7fffffff) + 0x1000; // rounded value
13 |
14 | if (value >= 0x47800000) // might be or become NaN/Inf
15 | { // avoid Inf due to rounding
16 |
17 | if (value < 0x7f800000) // was value but too large
18 | return sign | 0x7c00; // make it +/-Inf
19 | return sign
20 | | 0x7c00
21 | | // remains +/-Inf or NaN
22 | (bits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits
23 | }
24 | if (value >= 0x38800000) { // remains normalized value
25 | return sign | value - 0x38000000 >>> 13; // exp - 127 + 15
26 | }
27 | if (value < 0x33000000) { // too small for subnormal
28 | return sign; // becomes +/-0
29 | }
30 | value = (bits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc
31 |
32 | return sign
33 | | ((bits & 0x7fffff | 0x800000) // add subnormal bit
34 | + (0x800000 >>> value - 102) // round depending on cut off
35 | >>> 126 - value); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0
36 | }
37 |
38 | public static final float toFloat(int bits) {
39 | int mant = bits & 0x03ff; // 10 bits mantissa
40 | int exp = bits & 0x7c00; // 5 bits exponent
41 | if (exp == 0x7c00) { // NaN/Inf
42 | exp = 0x3fc00; // -> NaN/Inf
43 | } else if (exp != 0) { // normalized value
44 | exp += 0x1c000; // exp - 15 + 127
45 | } else if (mant != 0) { // && exp==0 -> subnormal
46 | exp = 0x1c400; // make it normal
47 | do {
48 | mant <<= 1; // mantissa * 2
49 | exp -= 0x400; // decrease exp by 1
50 | } while ((mant & 0x400) == 0); // while not normal
51 | mant &= 0x3ff; // discard subnormal bit
52 | } // else +/-0 -> +/-0
53 | return Float.intBitsToFloat( // combine all parts
54 | (bits & 0x8000) << 16 // sign << ( 31 - 15 )
55 | | (exp | mant) << 13); // value << ( 23 - 10 )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/modules/imports/src/test/scala/org/dhallj/imports/ToHeadersSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports
2 |
3 | import munit.FunSuite
4 | import org.dhallj.core.Expr
5 | import org.http4s.{Header, Headers}
6 |
7 | import scala.collection.JavaConverters._
8 |
9 | class ToHeadersSuite extends FunSuite {
10 |
11 | test("Success case") {
12 | val expr = Expr.makeNonEmptyListLiteral(
13 | Array(
14 | Expr.makeRecordLiteral(
15 | Map("header" -> Expr.makeTextLiteral("foo"), "value" -> Expr.makeTextLiteral("bar")).asJava.entrySet()
16 | ),
17 | Expr.makeRecordLiteral(
18 | Map("header" -> Expr.makeTextLiteral("baz"), "value" -> Expr.makeTextLiteral("x")).asJava.entrySet()
19 | )
20 | )
21 | )
22 |
23 | val expected = Headers(
24 | List(
25 | Header("foo", "bar"),
26 | Header("baz", "x")
27 | )
28 | )
29 |
30 | assertEquals(ToHeaders(expr), expected)
31 | }
32 |
33 | test("Success case 2") {
34 | val expr = Expr.makeNonEmptyListLiteral(
35 | Array(
36 | Expr.makeRecordLiteral(
37 | Map("mapKey" -> Expr.makeTextLiteral("foo"), "mapValue" -> Expr.makeTextLiteral("bar")).asJava.entrySet()
38 | ),
39 | Expr.makeRecordLiteral(
40 | Map("mapKey" -> Expr.makeTextLiteral("baz"), "mapValue" -> Expr.makeTextLiteral("x")).asJava.entrySet()
41 | )
42 | )
43 | )
44 |
45 | val expected = Headers(
46 | List(
47 | Header("foo", "bar"),
48 | Header("baz", "x")
49 | )
50 | )
51 |
52 | assertEquals(ToHeaders(expr), expected)
53 | }
54 |
55 | test("Failure case 1") {
56 | val expr = Expr.makeNonEmptyListLiteral(
57 | Array(
58 | Expr.makeRecordLiteral(
59 | Map("header" -> Expr.makeTextLiteral("foo"), "mapValue" -> Expr.makeTextLiteral("bar")).asJava.entrySet()
60 | ),
61 | Expr.makeRecordLiteral(
62 | Map("mapKey" -> Expr.makeTextLiteral("baz"), "value" -> Expr.makeTextLiteral("x")).asJava.entrySet()
63 | )
64 | )
65 | )
66 |
67 | val expected = Headers(Nil)
68 |
69 | assertEquals(ToHeaders(expr), expected)
70 | }
71 |
72 | test("Failure case 2") {
73 | val expr = Expr.makeTextLiteral("foo")
74 |
75 | val expected = Headers(Nil)
76 |
77 | assertEquals(ToHeaders(expr), expected)
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/Substitute.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.ArrayDeque;
4 | import java.util.Deque;
5 | import java.util.List;
6 | import org.dhallj.core.Expr;
7 | import org.dhallj.core.Visitor;
8 |
9 | /**
10 | * Substitutes an expression for all instances of a variable in another expression.
11 | *
12 | * Note that this visitor maintains internal state and instances should not be reused.
13 | */
14 | public final class Substitute extends Visitor.Identity {
15 | private final String name;
16 | private int index = 0;
17 | private final Deque replacementStack = new ArrayDeque<>();
18 |
19 | public Substitute(String name, Expr replacement) {
20 | this.name = name;
21 | this.replacementStack.push(replacement);
22 | }
23 |
24 | @Override
25 | public void bind(String name, Expr type) {
26 | this.replacementStack.push(this.replacementStack.peekFirst().increment(name));
27 |
28 | if (name.equals(this.name)) {
29 | this.index += 1;
30 | }
31 | }
32 |
33 | @Override
34 | public Expr onIdentifier(Expr self, String name, long index) {
35 | if (name.equals(this.name)) {
36 | if (index == this.index) {
37 | return this.replacementStack.peekFirst();
38 | } else if (index > this.index) {
39 | return Expr.makeIdentifier(name, index - 1);
40 | } else {
41 | return self;
42 | }
43 | } else {
44 | return self;
45 | }
46 | }
47 |
48 | @Override
49 | public Expr onLambda(String name, Expr type, Expr result) {
50 | this.replacementStack.pop();
51 |
52 | if (name.equals(this.name)) {
53 | this.index -= 1;
54 | }
55 |
56 | return Expr.makeLambda(name, type, result);
57 | }
58 |
59 | @Override
60 | public Expr onPi(String name, Expr type, Expr result) {
61 | this.replacementStack.pop();
62 |
63 | if (name.equals(this.name)) {
64 | this.index -= 1;
65 | }
66 |
67 | return Expr.makePi(name, type, result);
68 | }
69 |
70 | @Override
71 | public Expr onLet(List> bindings, Expr body) {
72 | for (Expr.LetBinding binding : bindings) {
73 | String name = binding.getName();
74 |
75 | this.replacementStack.pop();
76 |
77 | if (name.equals(this.name)) {
78 | this.index -= 1;
79 | }
80 | }
81 | return Expr.makeLet(bindings, body);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.github/workflows/clean.yml:
--------------------------------------------------------------------------------
1 | # This file was automatically generated by sbt-github-actions using the
2 | # githubWorkflowGenerate task. You should add and commit this file to
3 | # your git repository. It goes without saying that you shouldn't edit
4 | # this file by hand! Instead, if you wish to make changes, you should
5 | # change your sbt build configuration to revise the workflow description
6 | # to meet your needs, then regenerate this file.
7 |
8 | name: Clean
9 |
10 | on: push
11 |
12 | jobs:
13 | delete-artifacts:
14 | name: Delete Artifacts
15 | runs-on: ubuntu-latest
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 | steps:
19 | - name: Delete artifacts
20 | run: |
21 | # Customize those three lines with your repository and credentials:
22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }}
23 |
24 | # A shortcut to call GitHub API.
25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; }
26 |
27 | # A temporary file which receives HTTP response headers.
28 | TMPFILE=/tmp/tmp.$$
29 |
30 | # An associative array, key: artifact name, value: number of artifacts of that name.
31 | declare -A ARTCOUNT
32 |
33 | # Process all artifacts on this repository, loop on returned "pages".
34 | URL=$REPO/actions/artifacts
35 | while [[ -n "$URL" ]]; do
36 |
37 | # Get current page, get response headers in a temporary file.
38 | JSON=$(ghapi --dump-header $TMPFILE "$URL")
39 |
40 | # Get URL of next page. Will be empty if we are at the last page.
41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*/' -e 's/>.*//')
42 | rm -f $TMPFILE
43 |
44 | # Number of artifacts on this page:
45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') ))
46 |
47 | # Loop on all artifacts on this page.
48 | for ((i=0; $i < $COUNT; i++)); do
49 |
50 | # Get name of artifact and count instances of this name.
51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?")
52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1))
53 |
54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?")
55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") ))
56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size
57 | ghapi -X DELETE $REPO/actions/artifacts/$id
58 | done
59 | done
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeMerge.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.List;
4 | import java.util.Map.Entry;
5 | import org.dhallj.core.Expr;
6 | import org.dhallj.core.ExternalVisitor;
7 | import org.dhallj.core.Operator;
8 |
9 | final class BetaNormalizeMerge extends ExternalVisitor.Constant {
10 | private final List> handlers;
11 |
12 | private BetaNormalizeMerge(List> handlers) {
13 | super(null);
14 | this.handlers = handlers;
15 | }
16 |
17 | static final Expr apply(Expr handlers, Expr union, Expr type) {
18 | List> fields = Expr.Util.asRecordLiteral(handlers);
19 |
20 | Expr result = union.accept(new BetaNormalizeMerge(fields));
21 |
22 | if (result != null) {
23 | return result.accept(BetaNormalize.instance);
24 | } else {
25 |
26 | return Expr.makeMerge(handlers, union, type);
27 | }
28 | }
29 |
30 | @Override
31 | public Expr onFieldAccess(Expr base, String fieldName) {
32 | List> baseAsUnion = Expr.Util.asUnionType(base);
33 |
34 | if (baseAsUnion != null) {
35 | return merge(this.handlers, fieldName);
36 | } else {
37 | return null;
38 | }
39 | }
40 |
41 | @Override
42 | public Expr onApplication(Expr base, Expr arg) {
43 | Entry baseAsFieldAccess = Expr.Util.asFieldAccess(base);
44 | if (baseAsFieldAccess != null) {
45 | List> accessedAsUnion = Expr.Util.asUnionType(baseAsFieldAccess.getKey());
46 |
47 | if (accessedAsUnion != null) {
48 | return merge(this.handlers, baseAsFieldAccess.getValue(), arg);
49 | } else {
50 | return null;
51 | }
52 |
53 | } else {
54 | String baseAsBuiltIn = Expr.Util.asBuiltIn(base);
55 |
56 | if (baseAsBuiltIn != null) {
57 | if (baseAsBuiltIn.equals("Some")) {
58 | return merge(this.handlers, baseAsBuiltIn, arg);
59 | } else if (baseAsBuiltIn.equals("None")) {
60 | return merge(this.handlers, baseAsBuiltIn);
61 | }
62 | }
63 | return null;
64 | }
65 | }
66 |
67 | private static Expr merge(List> handlers, String fieldName) {
68 | return NormalizationUtilities.lookup(handlers, fieldName);
69 | }
70 |
71 | private static Expr merge(List> handlers, String fieldName, Expr arg) {
72 | Expr handler = NormalizationUtilities.lookup(handlers, fieldName);
73 | if (handler != null) {
74 | return Expr.makeApplication(handler, arg);
75 | } else {
76 | return null;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/modules/imports-mini/src/main/java/org/dhallj/imports/mini/Resolver.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports.mini;
2 |
3 | import org.dhallj.core.DhallException.ResolutionFailure;
4 | import org.dhallj.core.Expr;
5 | import java.nio.file.Path;
6 |
7 | public final class Resolver {
8 | public static final Expr resolve(Expr expr, boolean integrityChecks, Path currentPath)
9 | throws ResolutionFailure {
10 | return resolveWithVisitor(expr, new ResolutionVisitor.Filesystem(currentPath, integrityChecks));
11 | }
12 |
13 | public static final Expr resolve(Expr expr, boolean integrityChecks) throws ResolutionFailure {
14 | return resolve(expr, integrityChecks, null);
15 | }
16 |
17 | public static final Expr resolve(Expr expr) throws ResolutionFailure {
18 | return resolve(expr, true);
19 | }
20 |
21 | public static final Expr resolveFromResources(
22 | Expr expr, boolean integrityChecks, Path currentPath, ClassLoader classLoader)
23 | throws ResolutionFailure {
24 | return resolveWithVisitor(
25 | expr, new ResolutionVisitor.Resources(currentPath, integrityChecks, classLoader));
26 | }
27 |
28 | public static final Expr resolveFromResources(
29 | Expr expr, boolean integrityChecks, Path currentPath) throws ResolutionFailure {
30 | return resolveFromResources(
31 | expr, integrityChecks, currentPath, Resolver.class.getClassLoader());
32 | }
33 |
34 | public static final Expr resolveFromResources(Expr expr, boolean integrityChecks)
35 | throws ResolutionFailure {
36 | return resolveFromResources(expr, integrityChecks, null);
37 | }
38 |
39 | public static final Expr resolveFromResources(Expr expr) throws ResolutionFailure {
40 | return resolveFromResources(expr, true);
41 | }
42 |
43 | private static final Expr resolveWithVisitor(Expr expr, ResolutionVisitor visitor)
44 | throws ResolutionFailure {
45 | Expr result;
46 | try {
47 | result = expr.accept(visitor);
48 | } catch (ResolutionVisitor.WrappedParsingFailure e) {
49 | throw new ResolutionFailure(e.getMessage(), e.underlying);
50 | } catch (ResolutionVisitor.WrappedIOException e) {
51 | throw new ResolutionFailure(e.getMessage(), e.underlying);
52 | } catch (ResolutionVisitor.Missing e) {
53 | throw new ResolutionFailure(e.getMessage());
54 | } catch (ResolutionVisitor.MissingEnv e) {
55 | throw new ResolutionFailure(e.getMessage());
56 | } catch (ResolutionVisitor.IntegrityCheckException e) {
57 | throw new ResolutionFailure(e.getMessage());
58 | } catch (UnsupportedOperationException e) {
59 | throw new ResolutionFailure(e.getMessage());
60 | }
61 | return result;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/modules/core/src/test/java/org/dhallj/cbor/CborSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.cbor
2 |
3 | import co.nstant.in.cbor.{CborBuilder, CborEncoder}
4 | import java.io.ByteArrayOutputStream
5 | import java.math.BigInteger
6 | import munit.ScalaCheckSuite
7 | import org.scalacheck.Prop
8 |
9 | class CborSuite extends ScalaCheckSuite {
10 | def roundTripDouble(value: Double): Option[Double] = {
11 | val writer = new Writer.ByteArrayWriter()
12 | writer.writeDouble(value)
13 |
14 | val bytes = writer.getBytes
15 | val reader = new Reader.ByteArrayReader(bytes)
16 | reader.nextSymbol(DoubleValueVisitor)
17 | }
18 |
19 | def encodeDoubleWithCborJava(value: Double): Array[Byte] = {
20 | val stream = new ByteArrayOutputStream()
21 | new CborEncoder(stream).encode(new CborBuilder().add(value).build())
22 |
23 | return stream.toByteArray
24 | }
25 |
26 | property("Writer and Reader should round-trip doubles") {
27 | Prop.forAll((value: Double) => roundTripDouble(value) == Some(value))
28 | }
29 |
30 | property("Writer should agree with cbor-java") {
31 | Prop.forAll { (value: Double) =>
32 | val writer = new Writer.ByteArrayWriter()
33 | writer.writeDouble(value)
34 |
35 | writer.getBytes.sameElements(encodeDoubleWithCborJava(value))
36 | }
37 | }
38 |
39 | test("Writer and Reader should round-trip special-case doubles") {
40 | assertEquals(roundTripDouble(0.0), Some(0.0))
41 | assertEquals(roundTripDouble(-0.0), Some(-0.0))
42 | assertEquals(roundTripDouble(Double.PositiveInfinity), Some(Double.PositiveInfinity))
43 | assertEquals(roundTripDouble(Double.NegativeInfinity), Some(Double.NegativeInfinity))
44 | assert(roundTripDouble(Double.NaN).exists(_.isNaN))
45 | }
46 |
47 | object DoubleValueVisitor extends Visitor[Option[Double]] {
48 | def onUnsignedInteger(value: BigInteger): Option[Double] = None
49 | def onNegativeInteger(value: BigInteger): Option[Double] = None
50 | def onByteString(value: Array[Byte]): Option[Double] = None
51 | def onTextString(value: String): Option[Double] = None
52 | def onVariableArray(length: BigInteger, name: String): Option[Double] = None
53 | def onArray(length: BigInteger, tagI: BigInteger): Option[Double] = None
54 | def onMap(size: BigInteger): Option[Double] = None
55 | def onFalse: Option[Double] = None
56 | def onTrue: Option[Double] = None
57 | def onNull: Option[Double] = None
58 | def onHalfFloat(value: Float): Option[Double] = Some(value.toDouble)
59 | def onSingleFloat(value: Float): Option[Double] = Some(value.toDouble)
60 | def onDoubleFloat(value: Double): Option[Double] = Some(value)
61 | def onTag: Option[Double] = None
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeWith.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.AbstractMap.SimpleImmutableEntry;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.Comparator;
7 | import java.util.List;
8 | import java.util.Map.Entry;
9 | import org.dhallj.core.Expr;
10 |
11 | final class BetaNormalizeWith {
12 | static final Expr apply(Expr base, String[] path, Expr value) {
13 | List> baseAsRecordLiteral = Expr.Util.asRecordLiteral(base);
14 |
15 | if (baseAsRecordLiteral != null) {
16 | List> result = new ArrayList<>();
17 | boolean found = false;
18 |
19 | for (Entry field : baseAsRecordLiteral) {
20 | String key = field.getKey();
21 |
22 | if (key.equals(path[0])) {
23 | found = true;
24 |
25 | Expr newValue;
26 |
27 | if (path.length == 1) {
28 | newValue = value;
29 | } else {
30 | String[] remainingPath = new String[path.length - 1];
31 | System.arraycopy(path, 1, remainingPath, 0, path.length - 1);
32 |
33 | newValue =
34 | Expr.makeWith(field.getValue(), remainingPath, value)
35 | .accept(BetaNormalize.instance);
36 | }
37 |
38 | result.add(new SimpleImmutableEntry<>(key, newValue));
39 | } else {
40 | result.add(field);
41 | }
42 | }
43 |
44 | if (!found) {
45 | Expr newValue;
46 |
47 | if (path.length == 1) {
48 | newValue = value;
49 | } else {
50 | String[] remainingPath = new String[path.length - 1];
51 | System.arraycopy(path, 1, remainingPath, 0, path.length - 1);
52 |
53 | newValue =
54 | Expr.makeWith(Expr.Constants.EMPTY_RECORD_LITERAL, remainingPath, value)
55 | .accept(BetaNormalize.instance);
56 | }
57 |
58 | result.add(new SimpleImmutableEntry<>(path[0], newValue));
59 | }
60 |
61 | Entry[] resultArray = result.toArray(new Entry[result.size()]);
62 |
63 | Arrays.sort(resultArray, entryComparator);
64 |
65 | return Expr.makeRecordLiteral(resultArray);
66 | } else {
67 | return Expr.makeWith(base, path, value);
68 | }
69 | }
70 |
71 | /** Java 8 introduce {@code comparingByKey}, but we can roll our own pretty easily. */
72 | private static final Comparator> entryComparator =
73 | new Comparator>() {
74 | public int compare(Entry a, Entry b) {
75 | return a.getKey().compareTo(b.getKey());
76 | }
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/Operator.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core;
2 |
3 | /** Represents a Dhall operator. */
4 | public enum Operator {
5 | OR("||", 3, true),
6 | AND("&&", 7, true),
7 | EQUALS("==", 12, true),
8 | NOT_EQUALS("!=", 13, true),
9 | PLUS("+", 4, false),
10 | TIMES("*", 11, false),
11 | TEXT_APPEND("++", 5, false),
12 | LIST_APPEND("#", 6, false),
13 | COMBINE("\u2227", 8, false),
14 | PREFER("\u2afd", 9, false),
15 | COMBINE_TYPES("\u2a53", 10, false),
16 | IMPORT_ALT("?", 2, false),
17 | EQUIVALENT("\u2261", 1, false),
18 | COMPLETE("::", 0, false);
19 |
20 | private static final Operator[] values = values();
21 |
22 | private final String value;
23 | private final int precedence;
24 | private final boolean isBoolOperator;
25 |
26 | Operator(String value, int precedence, boolean isBoolOperator) {
27 | this.value = value;
28 | this.precedence = precedence;
29 | this.isBoolOperator = isBoolOperator;
30 | }
31 |
32 | public final boolean isBoolOperator() {
33 | return this.isBoolOperator;
34 | }
35 |
36 | public final int getLabel() {
37 | return this.ordinal();
38 | }
39 |
40 | public final int getPrecedence() {
41 | return this.precedence;
42 | }
43 |
44 | public final String toString() {
45 | return this.value;
46 | }
47 |
48 | public static final Operator fromLabel(int ordinal) {
49 | if (ordinal >= 0 && ordinal < values.length) {
50 | return values[ordinal];
51 | } else {
52 | return null;
53 | }
54 | }
55 |
56 | public static final Operator parse(String input) {
57 | if (input.equals(OR.value)) {
58 | return OR;
59 | } else if (input.equals(AND.value)) {
60 | return AND;
61 | } else if (input.equals(EQUALS.value)) {
62 | return EQUALS;
63 | } else if (input.equals(NOT_EQUALS.value)) {
64 | return NOT_EQUALS;
65 | } else if (input.equals(PLUS.value)) {
66 | return PLUS;
67 | } else if (input.equals(TIMES.value)) {
68 | return TIMES;
69 | } else if (input.equals(TEXT_APPEND.value)) {
70 | return TEXT_APPEND;
71 | } else if (input.equals(LIST_APPEND.value)) {
72 | return LIST_APPEND;
73 | } else if (input.equals(COMBINE.value) || input.equals("/\\")) {
74 | return COMBINE;
75 | } else if (input.equals(PREFER.value) || input.equals("//")) {
76 | return PREFER;
77 | } else if (input.equals(COMBINE_TYPES.value) || input.equals("//\\\\")) {
78 | return COMBINE_TYPES;
79 | } else if (input.equals(IMPORT_ALT.value)) {
80 | return IMPORT_ALT;
81 | } else if (input.equals(EQUIVALENT.value) || input.equals("===")) {
82 | return EQUIVALENT;
83 | } else if (input.equals(COMPLETE.value)) {
84 | return COMPLETE;
85 | } else {
86 | throw new IllegalArgumentException("No org.dhallj.core.Operator represented by " + input);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/modules/circe/src/main/scala/org/dhallj/circe/CirceHandler.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.circe
2 |
3 | import io.circe.Json
4 | import java.math.BigInteger
5 | import java.util.{ArrayDeque, ArrayList, Deque, LinkedHashMap, List}
6 | import org.dhallj.core.converters.JsonHandler
7 | import scala.collection.JavaConverters._
8 |
9 | sealed private trait Context {
10 | def add(key: String): Unit
11 | def add(value: Json): Unit
12 | def result: Json
13 | }
14 |
15 | final private class RootContext(value: Json) extends Context {
16 | def add(key: String): Unit = ()
17 | def add(value: Json): Unit = ()
18 | def result: Json = value
19 | }
20 |
21 | final private class ObjectContext() extends Context {
22 | private[this] var key: String = null
23 | private[this] val fields: LinkedHashMap[String, Json] = new LinkedHashMap()
24 |
25 | def add(key: String): Unit = this.key = key
26 | def add(value: Json): Unit = this.fields.put(this.key, value)
27 | def result: Json = Json.fromFields(this.fields.entrySet.asScala.map(entry => entry.getKey -> entry.getValue))
28 | }
29 |
30 | final private class ArrayContext() extends Context {
31 | private[this] val values: List[Json] = new ArrayList()
32 |
33 | def add(key: String): Unit = ()
34 | def add(value: Json): Unit = this.values.add(value)
35 | def result: Json = Json.fromValues(this.values.asScala)
36 | }
37 |
38 | class CirceHandler extends JsonHandler {
39 | private[this] val stack: Deque[Context] = new ArrayDeque()
40 |
41 | protected[this] def addValue(value: Json): Unit =
42 | if (stack.isEmpty) {
43 | stack.push(new RootContext(value))
44 | } else {
45 | stack.peek.add(value)
46 | }
47 |
48 | final def result: Json = stack.pop().result
49 |
50 | final def onNull(): Unit = addValue(Json.Null)
51 | final def onBoolean(value: Boolean): Unit = addValue(if (value) Json.True else Json.False)
52 | final def onNumber(value: BigInteger): Unit = addValue(Json.fromBigInt(new BigInt(value)))
53 | def onDouble(value: Double): Unit = addValue(Json.fromDoubleOrNull(value))
54 | final def onString(value: String): Unit = addValue(Json.fromString(value))
55 |
56 | final def onArrayStart(): Unit = stack.push(new ArrayContext())
57 |
58 | final def onArrayEnd(): Unit = {
59 | val current = stack.pop()
60 |
61 | if (stack.isEmpty) {
62 | stack.push(current)
63 | } else {
64 | stack.peek.add(current.result)
65 | }
66 | }
67 |
68 | final def onArrayElementGap(): Unit = ()
69 |
70 | final def onObjectStart(): Unit = stack.push(new ObjectContext())
71 |
72 | final def onObjectEnd(): Unit = {
73 | val current = stack.pop()
74 |
75 | if (stack.isEmpty) {
76 | stack.push(current)
77 | } else {
78 | stack.peek.add(current.result)
79 | }
80 | }
81 |
82 | final def onObjectField(name: String): Unit = stack.peek.add(name)
83 |
84 | final def onObjectFieldGap(): Unit = ()
85 | }
86 |
--------------------------------------------------------------------------------
/modules/imports/src/test/scala/org/dhallj/imports/CorsComplianceCheckSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports
2 |
3 | import java.net.URI
4 | import java.nio.file.Paths
5 |
6 | import cats.effect.IO
7 | import munit.FunSuite
8 | import org.dhallj.imports.ImportContext._
9 | import org.http4s.{Header, Headers}
10 |
11 | class CorsComplianceCheckSuite extends FunSuite {
12 |
13 | val fooOrigin = new URI("http://foo.org/foo.dhall")
14 | val fooOrigin8080 = new URI("http://foo.org:8080/foo.dhall")
15 | val fooOrigin2 = new URI("http://foo.org/baz.dhall")
16 | val barOrigin = new URI("http://bar.org/bar.dhall")
17 |
18 | val localPath = Paths.get("/foo/bar.dhall")
19 |
20 | test("Remote - same origin") {
21 | CorsComplianceCheck[IO](Remote(fooOrigin, null), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync()
22 | }
23 |
24 | test("Remote - different origin, allow *") {
25 | CorsComplianceCheck[IO](Remote(fooOrigin, null),
26 | Remote(barOrigin, null),
27 | Headers.of(Header("Access-Control-Allow-Origin", "*"))
28 | ).unsafeRunSync()
29 | }
30 |
31 | test("Remote - different origin, allow parent authority") {
32 | CorsComplianceCheck[IO](Remote(fooOrigin, null),
33 | Remote(barOrigin, null),
34 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org"))
35 | ).unsafeRunSync()
36 | }
37 |
38 | test("Remote - different origin".fail) {
39 | CorsComplianceCheck[IO](Remote(fooOrigin, null), Remote(barOrigin, null), Headers.empty).unsafeRunSync()
40 | }
41 |
42 | test("Remote - different origin, cors parent different authority".fail) {
43 | CorsComplianceCheck[IO](Remote(fooOrigin, null),
44 | Remote(barOrigin, null),
45 | Headers.of(Header("Access-Control-Allow-Origin", "http://bar.org"))
46 | ).unsafeRunSync()
47 | }
48 |
49 | test("Remote - different origin, cors parent different scheme".fail) {
50 | CorsComplianceCheck[IO](Remote(fooOrigin, null),
51 | Remote(barOrigin, null),
52 | Headers.of(Header("Access-Control-Allow-Origin", "https://foo.org"))
53 | ).unsafeRunSync()
54 | }
55 |
56 | test("Remote - different origin, cors parent different port".fail) {
57 | CorsComplianceCheck[IO](Remote(fooOrigin, null),
58 | Remote(barOrigin, null),
59 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org:8080"))
60 | ).unsafeRunSync()
61 | }
62 |
63 | test("Remote - different origin, cors parent different port 2".fail) {
64 | CorsComplianceCheck[IO](Remote(fooOrigin8080, null),
65 | Remote(barOrigin, null),
66 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org"))
67 | ).unsafeRunSync()
68 | }
69 |
70 | test("Local") {
71 | CorsComplianceCheck[IO](Local(localPath), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync()
72 | }
73 |
74 | test("Env") {
75 | CorsComplianceCheck[IO](Env("foo"), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeTextLiteral.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Iterator;
5 | import java.util.List;
6 | import org.dhallj.core.Expr;
7 | import org.dhallj.core.ExternalVisitor;
8 |
9 | final class BetaNormalizeTextLiteral {
10 | static final Expr apply(String[] parts, List interpolated) {
11 | if (parts.length == 1) {
12 | return Expr.makeTextLiteral(parts[0]);
13 | } else {
14 | int partsSize = 0;
15 | for (String part : parts) {
16 | partsSize += part.length();
17 | }
18 | int c = 0;
19 | if (partsSize == 0) {
20 | Expr notEmptyString = null;
21 | boolean tooMany = false;
22 | Iterator it = interpolated.iterator();
23 |
24 | while (it.hasNext() && !tooMany) {
25 | Expr next = it.next();
26 | String nextAsSimpleTextLiteral = Expr.Util.asSimpleTextLiteral(next);
27 |
28 | if (nextAsSimpleTextLiteral == null || nextAsSimpleTextLiteral.length() != 0) {
29 | if (notEmptyString == null) {
30 | notEmptyString = next;
31 | } else {
32 | tooMany = true;
33 | }
34 | }
35 | }
36 |
37 | if (!tooMany && notEmptyString != null) {
38 | return notEmptyString;
39 | }
40 | }
41 |
42 | List newParts = new ArrayList<>(parts.length);
43 | List newInterpolated = new ArrayList<>(parts.length - 1);
44 | newParts.add(parts[0]);
45 |
46 | boolean wasInlined = false;
47 | int partIndex = 1;
48 |
49 | for (Expr expr : interpolated) {
50 | wasInlined = expr.accept(new InlineInterpolatedTextLiteral(newParts, newInterpolated));
51 |
52 | if (!wasInlined) {
53 | newInterpolated.add(expr);
54 | newParts.add(parts[partIndex++]);
55 | } else {
56 | int lastIndex = newParts.size() - 1;
57 | String lastPart = newParts.get(lastIndex);
58 | newParts.set(lastIndex, lastPart + parts[partIndex++]);
59 | }
60 | }
61 |
62 | return Expr.makeTextLiteral(newParts.toArray(new String[newParts.size()]), newInterpolated);
63 | }
64 | }
65 |
66 | private static final class InlineInterpolatedTextLiteral
67 | extends ExternalVisitor.Constant {
68 | private final List newParts;
69 | private final List newInterpolated;
70 |
71 | InlineInterpolatedTextLiteral(List newParts, List newInterpolated) {
72 | super(false);
73 | this.newParts = newParts;
74 | this.newInterpolated = newInterpolated;
75 | }
76 |
77 | @Override
78 | public Boolean onText(String[] parts, Iterable interpolated) {
79 | int lastIndex = newParts.size() - 1;
80 | String lastPart = newParts.get(lastIndex);
81 | newParts.set(lastIndex, lastPart + parts[0]);
82 |
83 | Iterator it = interpolated.iterator();
84 |
85 | for (int i = 1; i < parts.length; i++) {
86 | newInterpolated.add(it.next());
87 | newParts.add(parts[i]);
88 | }
89 | return true;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/src/test/scala/org/dhallj/tests/acceptance/AcceptanceSuites.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.tests.acceptance
2 |
3 | class NormalizationSimpleSuite extends NormalizationUSuite("normalization/success/simple")
4 | class NormalizationRegressionSuite extends NormalizationSuite("normalization/success/regression")
5 | class NormalizationUnitSuite extends NormalizationUSuite("normalization/success/unit")
6 | class NormalizationSimplificationsSuite extends NormalizationSuite("normalization/success/simplifications")
7 | class NormalizationOtherSuite extends NormalizationSuite("normalization/success")
8 | class NormalizationHaskellTutorialSuite extends NormalizationSuite("normalization/success/haskell-tutorial", true)
9 |
10 | class HashingSimpleSuite extends HashingSuite("semantic-hash/success/simple")
11 | class HashingSimplificationsSuite extends HashingSuite("semantic-hash/success/simplifications")
12 | class HashingOtherSuite extends HashingSuite("semantic-hash/success")
13 | class HashingHaskellTutorialSuite extends HashingSuite("semantic-hash/success/haskell-tutorial", true)
14 | class HashingPreludeSuite extends HashingSuite("semantic-hash/success/prelude", true)
15 |
16 | class AlphaNormalizationUnitSuite extends AlphaNormalizationSuite("alpha-normalization/success/unit")
17 | class AlphaNormalizationRegressionSuite extends AlphaNormalizationSuite("alpha-normalization/success/regression")
18 |
19 | class TypeCheckingSimpleSuite extends ParsingTypeCheckingSuite("type-inference/success/simple")
20 | class TypeCheckingUnitSuite extends ParsingTypeCheckingSuite("type-inference/success/unit")
21 | class TypeCheckingRegressionSuite extends TypeCheckingSuite("type-inference/success/regression")
22 | class TypeCheckingOtherSuite extends TypeCheckingSuite("type-inference/success") {
23 | override def slow = Set("prelude")
24 | // Depends on http://csrng.net/, which is rate-limited (and also currently entirely down).
25 | override def ignored = Set("CacheImports", "CacheImportsCanonicalize")
26 | }
27 | class TypeCheckingFailureUnitSuite extends TypeCheckingFailureSuite("type-inference/failure/unit")
28 | class TypeCheckingPreludeSuite extends TypeCheckingSuite("type-inference/success/prelude", true)
29 |
30 | class ParsingUnitSuite extends ParsingSuite("parser/success/unit")
31 | class ParsingTextSuite extends ParsingSuite("parser/success/text")
32 | class ParsingOtherSuite extends ParsingSuite("parser/success")
33 |
34 | class ParsingFailureUnitSuite extends ParsingFailureSuite("parser/failure/unit")
35 | class ParsingFailureSpacingSuite extends ParsingFailureSuite("parser/failure/spacing")
36 | class ParsingFailureOtherSuite extends ParsingFailureSuite("parser/failure")
37 |
38 | class BinaryDecodingUnitSuite extends BinaryDecodingSuite("binary-decode/success/unit")
39 | class BinaryDecodingImportsUnitSuite extends BinaryDecodingSuite("binary-decode/success/unit/imports")
40 | class BinaryDecodingFailureUnitSuite extends BinaryDecodingFailureSuite("binary-decode/failure/unit")
41 |
42 | class ImportResolutionSuccessSuite extends ImportResolutionSuite("import/success")
43 | class ImportResolutionSuccessUnitSuite extends ImportResolutionSuite("import/success/unit")
44 | class ImportResolutionSuccessUnitAsLocationSuite extends ImportResolutionSuite("import/success/unit/asLocation")
45 |
--------------------------------------------------------------------------------
/modules/yaml/src/test/scala/org/dhallj/yaml/YamlConverterSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.yaml
2 |
3 | import munit.ScalaCheckSuite
4 | import org.dhallj.ast._
5 | import org.dhallj.core.Expr
6 | import org.dhallj.parser.DhallParser
7 | import org.scalacheck.Prop
8 | import org.yaml.snakeyaml.DumperOptions
9 |
10 | class YamlConverterSuite extends ScalaCheckSuite {
11 | property("convert integers") {
12 | Prop.forAll { (value: BigInt) =>
13 | val asDhall = IntegerLiteral(value.underlying)
14 | Option(YamlConverter.toYamlString(asDhall)) == Some(value.toString + "\n")
15 | }
16 | }
17 |
18 | test("convert nested lists") {
19 | val expr = DhallParser.parse("[[]: List Bool]")
20 |
21 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("- []\n"))
22 | }
23 |
24 | test("convert None") {
25 | val expr = DhallParser.parse("None Bool")
26 |
27 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("null\n"))
28 | }
29 |
30 | test("convert Some") {
31 | val expr = DhallParser.parse("""Some "foo"""")
32 |
33 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("foo\n"))
34 | }
35 |
36 | test("convert records") {
37 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}")
38 | val expected =
39 | """|foo:
40 | |- bar:
41 | | - 1
42 | |- bar:
43 | | - 1
44 | | - 2
45 | | - 3
46 | |""".stripMargin
47 |
48 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some(expected))
49 | }
50 |
51 | test("convert unions (nullary constructors)") {
52 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize()
53 |
54 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("- bar\n"))
55 | }
56 |
57 | test("convert unions") {
58 | val expr1 = DhallParser.parse("[.foo True]").normalize()
59 |
60 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("- true\n"))
61 | }
62 |
63 | test("convert text containing newlines") {
64 | val expr1 = DhallParser.parse(""" { a = "foo\nbar" } """).normalize()
65 |
66 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("a: |-\n foo\n bar\n"))
67 | }
68 |
69 | test("convert test containing quotes") {
70 | val expr1 = DhallParser.parse(""" { a = "\"" } """).normalize()
71 | val expr2 = DhallParser.parse(""" { a = "\"\n" } """).normalize()
72 |
73 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("a: '\"'\n"))
74 | assert(clue(Option(YamlConverter.toYamlString(expr2))) == Some("a: |\n \"\n"))
75 | }
76 |
77 | test("convert text containing newlines and respect SnakeYAML configuration") {
78 | val expr1 = DhallParser.parse(""" { a = "foo\nbar" } """).normalize()
79 | val expected = "\"a\": \"foo\\nbar\"\n"
80 |
81 | val options = new DumperOptions()
82 | options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED)
83 |
84 | assert(clue(Option(YamlConverter.toYamlString(expr1, options))) == clue(Some(expected)))
85 | }
86 |
87 | test("fail safely on unconvertible expressions") {
88 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x"))
89 |
90 | assert(Option(YamlConverter.toYamlString(expr1)) == None)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeProjection.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.Arrays;
4 | import java.util.HashSet;
5 | import java.util.List;
6 | import java.util.Map;
7 | import java.util.Map.Entry;
8 | import java.util.Set;
9 | import java.util.TreeMap;
10 | import java.util.TreeSet;
11 | import org.dhallj.core.Expr;
12 | import org.dhallj.core.ExternalVisitor;
13 | import org.dhallj.core.Operator;
14 |
15 | final class BetaNormalizeProjection extends ExternalVisitor.Constant {
16 | private final String[] fieldNames;
17 |
18 | private BetaNormalizeProjection(String[] fieldNames) {
19 | super(null);
20 | this.fieldNames = fieldNames;
21 | }
22 |
23 | static final Expr apply(Expr base, final String[] fieldNames) {
24 | if (fieldNames.length == 0) {
25 | return Expr.Constants.EMPTY_RECORD_LITERAL;
26 | }
27 |
28 | Expr result = base.accept(new BetaNormalizeProjection(fieldNames));
29 |
30 | if (result != null) {
31 | return result;
32 | } else {
33 | // We have to sort the field names if we can't reduce.
34 | String[] newFieldNames = new String[fieldNames.length];
35 | System.arraycopy(fieldNames, 0, newFieldNames, 0, fieldNames.length);
36 | Arrays.sort(newFieldNames);
37 | return Expr.makeProjection(base, newFieldNames);
38 | }
39 | }
40 |
41 | @Override
42 | public Expr onRecord(Iterable> fields, int size) {
43 | Set fieldNameSet = new HashSet(this.fieldNames.length);
44 |
45 | for (String fieldName : this.fieldNames) {
46 | fieldNameSet.add(fieldName);
47 | }
48 |
49 | Map selected = new TreeMap();
50 |
51 | for (Entry entry : fields) {
52 | if (fieldNameSet.contains(entry.getKey())) {
53 | selected.put(entry.getKey(), entry.getValue());
54 | }
55 | }
56 | return Expr.makeRecordLiteral(selected.entrySet());
57 | }
58 |
59 | @Override
60 | public Expr onProjection(Expr base, String[] fieldNames) {
61 | return Expr.makeProjection(base, this.fieldNames).accept(BetaNormalize.instance);
62 | }
63 |
64 | @Override
65 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) {
66 | if (operator.equals(Operator.PREFER)) {
67 | List> rhsFields = Expr.Util.asRecordLiteral(rhs);
68 | if (rhsFields != null) {
69 |
70 | Set rhsKeys = new HashSet();
71 | Set leftFields = new TreeSet();
72 | Set rightFields = new TreeSet();
73 |
74 | for (Entry entry : rhsFields) {
75 | rhsKeys.add(entry.getKey());
76 | }
77 |
78 | for (String fieldName : this.fieldNames) {
79 | if (rhsKeys.contains(fieldName)) {
80 | rightFields.add(fieldName);
81 | } else {
82 | leftFields.add(fieldName);
83 | }
84 | }
85 |
86 | return Expr.makeOperatorApplication(
87 | Operator.PREFER,
88 | Expr.makeProjection(lhs, leftFields.toArray(new String[leftFields.size()])),
89 | Expr.makeProjection(rhs, rightFields.toArray(new String[leftFields.size()])))
90 | .accept(BetaNormalize.instance);
91 | }
92 | }
93 | return null;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/modules/imports/src/main/scala/org/dhallj/imports/ImportCache.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports
2 |
3 | import java.nio.file.{Files, Path, Paths}
4 |
5 | import cats.effect.Sync
6 | import cats.implicits._
7 |
8 | trait ImportCache[F[_]] {
9 | def get(key: Array[Byte]): F[Option[Array[Byte]]]
10 |
11 | def put(key: Array[Byte], value: Array[Byte]): F[Unit]
12 | }
13 |
14 | object ImportCache {
15 |
16 | /*
17 | * Improves the ergonomics when resolving imports if we don't have to check
18 | * if the cache exists. So if we fail to construct an imports cache,
19 | * we warn and return this instead.
20 | */
21 | class NoopImportCache[F[_]](implicit F: Sync[F]) extends ImportCache[F] {
22 | override def get(key: Array[Byte]): F[Option[Array[Byte]]] = F.pure(None)
23 |
24 | override def put(key: Array[Byte], value: Array[Byte]): F[Unit] = F.unit
25 | }
26 |
27 | private class Impl[F[_]](rootDir: Path)(implicit F: Sync[F]) extends ImportCache[F] {
28 | override def get(key: Array[Byte]): F[Option[Array[Byte]]] = {
29 | val p = path(key)
30 | if (Files.exists(p)) {
31 | F.delay(Some(Files.readAllBytes(p)))
32 | } else {
33 | F.pure(None)
34 | }
35 | }
36 |
37 | override def put(key: Array[Byte], value: Array[Byte]): F[Unit] =
38 | F.delay(Files.write(path(key), value))
39 |
40 | private def path(key: Array[Byte]): Path = rootDir.resolve(s"1220${toHex(key)}")
41 |
42 | private def toHex(bs: Array[Byte]): String = {
43 | val sb = new StringBuilder
44 | for (b <- bs) {
45 | sb.append(String.format("%02x", Byte.box(b)))
46 | }
47 | sb.toString
48 | }
49 | }
50 |
51 | def apply[F[_] <: AnyRef](rootDir: Path)(implicit F: Sync[F]): F[Option[ImportCache[F]]] =
52 | for {
53 | _ <- if (!Files.exists(rootDir)) F.delay(Files.createDirectories(rootDir)) else F.unit
54 | perms <- F.delay(Files.isReadable(rootDir) && Files.isWritable(rootDir))
55 | } yield (if (perms) Some(new Impl[F](rootDir)) else None)
56 |
57 | def apply[F[_] <: AnyRef](cacheName: String)(implicit F: Sync[F]): F[ImportCache[F]] = {
58 | def makeCacheFromEnvVar(env: String, relativePath: String): F[Option[ImportCache[F]]] =
59 | for {
60 | envValO <- F.delay(sys.env.get(env))
61 | cache <- envValO.fold(F.pure(Option.empty[ImportCache[F]]))(envVal =>
62 | for {
63 | rootDir <- F.pure(Paths.get(envVal, relativePath, cacheName))
64 | c <- apply(rootDir)
65 | } yield c
66 | )
67 | } yield cache
68 |
69 | def backupCache =
70 | for {
71 | cacheO <-
72 | if (isWindows)
73 | makeCacheFromEnvVar("LOCALAPPDATA", "")
74 | else makeCacheFromEnvVar("HOME", ".cache")
75 | cache <- cacheO.fold[F[ImportCache[F]]](F.as(warnCacheNotCreated, new NoopImportCache[F]))(F.pure)
76 | } yield cache
77 |
78 | def isWindows = System.getProperty("os.name").toLowerCase.contains("Windows")
79 |
80 | def warnCacheNotCreated: F[Unit] =
81 | F.delay(
82 | println(
83 | "WARNING: failed to create cache at either $XDG_CACHE_HOME}/dhall or $HOME/.cache/dhall. Are these locations writable?"
84 | )
85 | )
86 |
87 | for {
88 | xdgCache <- makeCacheFromEnvVar("XDG_CACHE_HOME", "")
89 | cache <- xdgCache.fold(backupCache)(F.pure)
90 | } yield cache
91 |
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/modules/jawn/src/test/scala/org/dhallj/jawn/JawnConverterSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.jawn
2 |
3 | import io.circe.Json
4 | import io.circe.jawn.CirceSupportParser
5 | import io.circe.syntax._
6 | import munit.ScalaCheckSuite
7 | import org.dhallj.ast._
8 | import org.dhallj.core.Expr
9 | import org.dhallj.parser.DhallParser
10 | import org.scalacheck.Prop
11 |
12 | class JawnConverterSuite extends ScalaCheckSuite {
13 | val converter = new JawnConverter(CirceSupportParser.facade)
14 |
15 | property("convert integers") {
16 | Prop.forAll { (value: BigInt) =>
17 | val asDhall = IntegerLiteral(value.underlying)
18 | converter(asDhall) == Some(value.asJson)
19 | }
20 | }
21 |
22 | property("convert lists of integers") {
23 | Prop.forAll { (values: Vector[BigInt]) =>
24 | val asDhall = if (values.isEmpty) {
25 | EmptyListLiteral(Expr.Constants.INTEGER)
26 | } else {
27 | val exprs = values.map(value => IntegerLiteral(value.underlying))
28 | NonEmptyListLiteral(exprs.head, exprs.tail)
29 | }
30 | converter(asDhall) == Some(values.asJson)
31 | }
32 | }
33 |
34 | property("convert lists of doubles") {
35 | Prop.forAll { (values: Vector[Double]) =>
36 | val asDhall = if (values.isEmpty) {
37 | EmptyListLiteral(Expr.Constants.DOUBLE)
38 | } else {
39 | val exprs = values.map(DoubleLiteral(_))
40 | NonEmptyListLiteral(exprs.head, exprs.tail)
41 | }
42 | converter(asDhall) == Some(values.asJson)
43 | }
44 | }
45 |
46 | property("convert lists of booleans") {
47 | Prop.forAll { (values: Vector[Boolean]) =>
48 | val asDhall = if (values.isEmpty) {
49 | EmptyListLiteral(Expr.Constants.BOOL)
50 | } else {
51 | val exprs = values.map(BoolLiteral(_))
52 | NonEmptyListLiteral(exprs.head, exprs.tail)
53 | }
54 | converter(asDhall) == Some(values.asJson)
55 | }
56 | }
57 |
58 | test("convert nested lists") {
59 | val expr = DhallParser.parse("[[]: List Bool]")
60 |
61 | assert(converter(expr) == Some(Json.arr(Json.arr())))
62 | }
63 |
64 | test("convert None") {
65 | val expr = DhallParser.parse("None Bool")
66 |
67 | assert(converter(expr) == Some(Json.Null))
68 | }
69 |
70 | test("convert Some") {
71 | val expr = DhallParser.parse("""Some "foo"""")
72 |
73 | assert(converter(expr) == Some(Json.fromString("foo")))
74 | }
75 |
76 | test("convert records") {
77 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}")
78 | val Right(json1) = io.circe.jawn.parse("""{"foo": [{"bar": [1]}, {"bar": [1, 2, 3]}]}""")
79 |
80 | assert(converter(expr1) == Some(json1))
81 | }
82 |
83 | test("convert unions (nullary constructors)") {
84 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize()
85 | val Right(json1) = io.circe.jawn.parse("""["bar"]""")
86 |
87 | assert(converter(expr1) == Some(json1))
88 | }
89 |
90 | test("convert unions") {
91 | val expr1 = DhallParser.parse("[.foo True]").normalize()
92 | val Right(json1) = io.circe.jawn.parse("""[true]""")
93 |
94 | assert(converter(expr1) == Some(json1))
95 | }
96 |
97 | test("fail safely on unconvertible expressions") {
98 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x"))
99 |
100 | assert(converter(expr1) == None)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/converters/JsonHandler.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.converters;
2 |
3 | import java.io.PrintWriter;
4 | import java.math.BigInteger;
5 |
6 | public interface JsonHandler {
7 | void onNull();
8 |
9 | void onBoolean(boolean value);
10 |
11 | void onNumber(BigInteger value);
12 |
13 | void onDouble(double value);
14 |
15 | void onString(String value);
16 |
17 | void onArrayStart();
18 |
19 | void onArrayEnd();
20 |
21 | void onArrayElementGap();
22 |
23 | void onObjectStart();
24 |
25 | void onObjectEnd();
26 |
27 | void onObjectField(String name);
28 |
29 | void onObjectFieldGap();
30 |
31 | public static final class CompactPrinter implements JsonHandler {
32 | private final PrintWriter writer;
33 |
34 | public CompactPrinter(PrintWriter writer) {
35 | this.writer = writer;
36 | }
37 |
38 | public void onNull() {
39 | this.writer.print("null");
40 | }
41 |
42 | public void onBoolean(boolean value) {
43 | this.writer.print(value);
44 | }
45 |
46 | public void onNumber(BigInteger value) {
47 | this.writer.print(value.toString());
48 | }
49 |
50 | public void onDouble(double value) {
51 | this.writer.print(value);
52 | }
53 |
54 | public void onString(String value) {
55 | this.writer.printf("\"%s\"", value);
56 | }
57 |
58 | public void onArrayStart() {
59 | this.writer.print("[");
60 | }
61 |
62 | public void onArrayEnd() {
63 | this.writer.print("]");
64 | }
65 |
66 | public void onArrayElementGap() {
67 | this.writer.print(",");
68 | }
69 |
70 | public void onObjectStart() {
71 | this.writer.print("{");
72 | }
73 |
74 | public void onObjectEnd() {
75 | this.writer.print("}");
76 | }
77 |
78 | public void onObjectField(String name) {
79 | this.writer.printf("\"%s\":", name);
80 | }
81 |
82 | public void onObjectFieldGap() {
83 | this.writer.print(",");
84 | }
85 | }
86 |
87 | public static final class CompactStringPrinter implements JsonHandler {
88 | private final StringBuilder builder;
89 |
90 | public CompactStringPrinter() {
91 | this.builder = new StringBuilder();
92 | }
93 |
94 | public String toString() {
95 | return this.builder.toString();
96 | }
97 |
98 | public void onNull() {
99 | this.builder.append("null");
100 | }
101 |
102 | public void onBoolean(boolean value) {
103 | this.builder.append(value);
104 | }
105 |
106 | public void onNumber(BigInteger value) {
107 | this.builder.append(value.toString());
108 | }
109 |
110 | public void onDouble(double value) {
111 | this.builder.append(value);
112 | }
113 |
114 | public void onString(String value) {
115 | this.builder.append(String.format("\"%s\"", value));
116 | }
117 |
118 | public void onArrayStart() {
119 | this.builder.append("[");
120 | }
121 |
122 | public void onArrayEnd() {
123 | this.builder.append("]");
124 | }
125 |
126 | public void onArrayElementGap() {
127 | this.builder.append(",");
128 | }
129 |
130 | public void onObjectStart() {
131 | this.builder.append("{");
132 | }
133 |
134 | public void onObjectEnd() {
135 | this.builder.append("}");
136 | }
137 |
138 | public void onObjectField(String name) {
139 | this.builder.append(String.format("\"%s\":", name));
140 | }
141 |
142 | public void onObjectFieldGap() {
143 | this.builder.append(",");
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/AlphaNormalize.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.ArrayList;
4 | import java.util.HashMap;
5 | import java.util.LinkedList;
6 | import java.util.List;
7 | import java.util.Map;
8 | import org.dhallj.core.Expr;
9 | import org.dhallj.core.Visitor;
10 |
11 | /**
12 | * Performs alpha normalization.
13 | *
14 | * Morally this is equivalent to the following (on non-underscore bindings):
15 | *
16 | *
17 | * input.increment("_").substitute(name, 0, Expr.Constants.UNDERSCORE).decrement(name);
18 | *
19 | *
20 | * The implementation here is necessary to fit the visitor API.
21 | *
22 | * Note that this visitor maintains internal state and instances should not be reused.
23 | */
24 | public final class AlphaNormalize extends Visitor.Identity {
25 | // We interpret any underscores as implicitly having this index added to their own.
26 | private int newUnderscoreDepth = 0;
27 |
28 | // The total number of underscores.
29 | private int underscoreDepth = 0;
30 |
31 | // We change any other name to an underscore whose index we compute from the depth we track here.
32 | private final Map> nameDepths =
33 | new HashMap>();
34 |
35 | @Override
36 | public Expr onIdentifier(Expr self, String name, long index) {
37 | if (name.equals("_")) {
38 | return Expr.makeIdentifier(name, index + this.newUnderscoreDepth);
39 | } else {
40 | LinkedList depths = this.nameDepths.get(name);
41 |
42 | if (depths == null) {
43 | return self;
44 | } else if (index < depths.size()) {
45 | return Expr.makeIdentifier("_", underscoreDepth - depths.get((int) index));
46 | } else {
47 | return Expr.makeIdentifier(name, index - depths.size());
48 | }
49 | }
50 | }
51 |
52 | @Override
53 | public void bind(String name, Expr type) {
54 | this.underscoreDepth += 1;
55 | if (!name.equals("_")) {
56 | this.newUnderscoreDepth += 1;
57 |
58 | LinkedList nameDepth = this.nameDepths.get(name);
59 | if (nameDepth == null) {
60 | nameDepth = new LinkedList();
61 | nameDepths.put(name, nameDepth);
62 | }
63 | nameDepth.push(this.underscoreDepth);
64 | }
65 | }
66 |
67 | @Override
68 | public Expr onLambda(String name, Expr type, Expr result) {
69 | this.underscoreDepth -= 1;
70 | if (!name.equals("_")) {
71 | this.newUnderscoreDepth -= 1;
72 | this.nameDepths.get(name).pop();
73 | }
74 | return Expr.makeLambda("_", type, result);
75 | }
76 |
77 | @Override
78 | public Expr onPi(String name, Expr type, Expr result) {
79 | this.underscoreDepth -= 1;
80 | if (!name.equals("_")) {
81 | this.newUnderscoreDepth -= 1;
82 | this.nameDepths.get(name).pop();
83 | }
84 | return Expr.makePi("_", type, result);
85 | }
86 |
87 | @Override
88 | public Expr onLet(List> bindings, Expr body) {
89 | List> newBindings = new ArrayList<>(bindings.size());
90 |
91 | for (Expr.LetBinding binding : bindings) {
92 | String name = binding.getName();
93 |
94 | this.underscoreDepth -= 1;
95 | if (!name.equals("_")) {
96 | this.newUnderscoreDepth -= 1;
97 | this.nameDepths.get(name).pop();
98 | }
99 |
100 | newBindings.add(new Expr.LetBinding<>("_", binding.getType(), binding.getValue()));
101 | }
102 |
103 | return Expr.makeLet(newBindings, body);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/modules/circe/src/test/scala/org/dhallj/circe/CirceConverterSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.circe
2 |
3 | import io.circe.Json
4 | import io.circe.syntax._
5 | import io.circe.testing.instances._
6 | import munit.ScalaCheckSuite
7 | import org.dhallj.ast._
8 | import org.dhallj.core.Expr
9 | import org.dhallj.parser.DhallParser
10 | import org.scalacheck.Prop
11 |
12 | class CirceConverterSuite extends ScalaCheckSuite {
13 | property("round-trip Json values through Dhall expressions") {
14 | Prop.forAll { (value: Json) =>
15 | val cleanedJson = value.foldWith(JsonCleaner)
16 | val asDhall = Converter(cleanedJson)
17 | Converter(asDhall) == Some(cleanedJson)
18 | }
19 | }
20 |
21 | property("convert integers") {
22 | Prop.forAll { (value: BigInt) =>
23 | val asDhall = IntegerLiteral(value.underlying)
24 | Converter(asDhall) == Some(value.asJson)
25 | }
26 | }
27 |
28 | property("convert lists of integers") {
29 | Prop.forAll { (values: Vector[BigInt]) =>
30 | val asDhall = if (values.isEmpty) {
31 | EmptyListLiteral(Expr.Constants.INTEGER)
32 | } else {
33 | val exprs = values.map(value => IntegerLiteral(value.underlying))
34 | NonEmptyListLiteral(exprs.head, exprs.tail)
35 | }
36 | Converter(asDhall) == Some(values.asJson)
37 | }
38 | }
39 |
40 | property("convert lists of doubles") {
41 | Prop.forAll { (values: Vector[Double]) =>
42 | val asDhall = if (values.isEmpty) {
43 | EmptyListLiteral(Expr.Constants.DOUBLE)
44 | } else {
45 | val exprs = values.map(DoubleLiteral(_))
46 | NonEmptyListLiteral(exprs.head, exprs.tail)
47 | }
48 | Converter(asDhall) == Some(values.asJson)
49 | }
50 | }
51 |
52 | property("convert lists of booleans") {
53 | Prop.forAll { (values: Vector[Boolean]) =>
54 | val asDhall = if (values.isEmpty) {
55 | EmptyListLiteral(Expr.Constants.BOOL)
56 | } else {
57 | val exprs = values.map(BoolLiteral(_))
58 | NonEmptyListLiteral(exprs.head, exprs.tail)
59 | }
60 | Converter(asDhall) == Some(values.asJson)
61 | }
62 | }
63 |
64 | test("convert nested lists") {
65 | val expr = DhallParser.parse("[[]: List Bool]")
66 |
67 | assert(Converter(expr) == Some(Json.arr(Json.arr())))
68 | }
69 |
70 | test("convert None") {
71 | val expr = DhallParser.parse("None Bool")
72 |
73 | assert(Converter(expr) == Some(Json.Null))
74 | }
75 |
76 | test("convert Some") {
77 | val expr = DhallParser.parse("""Some "foo"""")
78 |
79 | assert(Converter(expr) == Some(Json.fromString("foo")))
80 | }
81 |
82 | test("convert records") {
83 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}")
84 | val Right(json1) = io.circe.jawn.parse("""{"foo": [{"bar": [1]}, {"bar": [1, 2, 3]}]}""")
85 |
86 | assert(Converter(expr1) == Some(json1))
87 | }
88 |
89 | test("convert unions (nullary constructors)") {
90 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize()
91 | val Right(json1) = io.circe.jawn.parse("""["bar"]""")
92 |
93 | assert(Converter(expr1) == Some(json1))
94 | }
95 |
96 | test("convert unions") {
97 | val expr1 = DhallParser.parse("[.foo True]").normalize()
98 | val Right(json1) = io.circe.jawn.parse("""[true]""")
99 |
100 | assert(Converter(expr1) == Some(json1))
101 | }
102 |
103 | test("fail safely on unconvertible expressions") {
104 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x"))
105 |
106 | assert(Converter(expr1) == None)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/modules/parser/src/test/scala/org/dhallj/parser/DhallParserSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.parser
2 |
3 | import java.net.URI
4 | import java.nio.file.Paths
5 |
6 | import munit.{FunSuite, Ignore}
7 | import org.dhallj.core.DhallException.ParsingFailure
8 | import org.dhallj.core.Expr
9 | import org.dhallj.core.Expr.ImportMode
10 |
11 | class DhallParserSuite extends FunSuite() {
12 | test("parse empty list with annotation on element type") {
13 | val expected = Expr.makeEmptyListLiteral(
14 | Expr.makeAnnotated(Expr.makeApplication(Expr.Constants.LIST, Expr.Constants.NATURAL), Expr.Constants.TYPE)
15 | )
16 | // Output from dhall-haskell.
17 | val expectedBytes: Array[Byte] = Array(-126, 24, 28, -125, 24, 26, -125, 0, 100, 76, 105, 115, 116, 103, 78, 97,
18 | 116, 117, 114, 97, 108, 100, 84, 121, 112, 101)
19 | val parsed = DhallParser.parse("[]: List Natural: Type")
20 |
21 | assert(parsed == expected)
22 | assert(parsed.getEncodedBytes.sameElements(expectedBytes))
23 | }
24 |
25 | test("parse toMap with empty record with annotation on type") {
26 | // Output from dhall-haskell.
27 | val expectedBytes: Array[Byte] = Array(-125, 24, 27, -126, 7, -96, -125, 24, 26, -125, 0, 100, 76, 105, 115, 116,
28 | -126, 7, -94, 102, 109, 97, 112, 75, 101, 121, 100, 84, 101, 120, 116, 104, 109, 97, 112, 86, 97, 108, 117, 101,
29 | 100, 66, 111, 111, 108, 100, 84, 121, 112, 101)
30 | val parsed = DhallParser.parse("toMap {}: List { mapKey : Text, mapValue: Bool }: Type")
31 |
32 | assert(parsed.getEncodedBytes.sameElements(expectedBytes))
33 | }
34 |
35 | test("parse toMap with empty non-record with annotation on type") {
36 | // Output from dhall-haskell.
37 | val expectedBytes: Array[Byte] = Array(-125, 24, 27, -126, 8, -95, 97, 97, -11, -125, 24, 26, -125, 0, 100, 76, 105,
38 | 115, 116, -126, 7, -94, 102, 109, 97, 112, 75, 101, 121, 100, 84, 101, 120, 116, 104, 109, 97, 112, 86, 97, 108,
39 | 117, 101, 100, 66, 111, 111, 108, 100, 84, 121, 112, 101)
40 | val parsed = DhallParser.parse("toMap {a=True}: List { mapKey : Text, mapValue: Bool }: Type")
41 |
42 | assert(parsed.getEncodedBytes.sameElements(expectedBytes))
43 | }
44 |
45 | test("parse merge with annotation on type") {
46 | // Output from dhall-haskell.
47 | val expectedBytes: Array[Byte] = Array(-124, 6, -126, 8, -95, 97, 97, -126, 15, 1, -125, 9, -126, 11, -95, 97, 97,
48 | -10, 97, 97, -125, 24, 26, 103, 78, 97, 116, 117, 114, 97, 108, 100, 84, 121, 112, 101)
49 | val parsed = DhallParser.parse("merge {a=1} .a: Natural: Type")
50 |
51 | assert(clue(parsed.getEncodedBytes).sameElements(clue(expectedBytes)))
52 | }
53 |
54 | test("parse IPv6 address") {
55 | val expected = Expr.makeRemoteImport(new URI("https://[0:0:0:0:0:0:0:1]/"), null, Expr.ImportMode.CODE, null)
56 |
57 | assert(DhallParser.parse("https://[0:0:0:0:0:0:0:1]/") == expected)
58 | }
59 |
60 | test("parse $ in double-quoted text literals") {
61 | val expected = Expr.makeTextLiteral("$ $ $100 $ $")
62 |
63 | assert(DhallParser.parse("""let x = "100" in "$ $ $${x} $ $" """) == expected)
64 | }
65 |
66 | test("parse # in double-quoted text literals") {
67 | val expected = Expr.makeTextLiteral("# # # $ % ^ #")
68 |
69 | assert(DhallParser.parse(""""# # # $ % ^ #"""") == expected)
70 | }
71 |
72 | test("parse classpath import") {
73 | val expected = Expr.makeClasspathImport(Paths.get("/foo/bar.dhall"), ImportMode.RAW_TEXT, null)
74 |
75 | assert(DhallParser.parse("classpath:/foo/bar.dhall as Text") == expected)
76 | }
77 |
78 | test("fail on URLs with quoted paths") {
79 | intercept[ParsingFailure](DhallParser.parse("https://example.com/foo/\"bar?baz\"?qux"))
80 | }
81 |
82 | test("handle single-quoted escape sequences") {
83 | val expected = Expr.makeTextLiteral("foo '' bar ${ baz '''' qux")
84 |
85 | val input = """''
86 | foo ''' bar ''${ baz '''''' qux''"""
87 |
88 | assertEquals(DhallParser.parse(input): Expr, expected)
89 |
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeFieldAccess.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.Map.Entry;
6 | import org.dhallj.core.Expr;
7 | import org.dhallj.core.ExternalVisitor;
8 | import org.dhallj.core.Operator;
9 |
10 | final class BetaNormalizeFieldAccess extends ExternalVisitor.Constant {
11 | private final String fieldName;
12 |
13 | public BetaNormalizeFieldAccess(String fieldName) {
14 | super(null);
15 | this.fieldName = fieldName;
16 | }
17 |
18 | static final Expr apply(Expr base, String fieldName) {
19 | Expr result = base.accept(new BetaNormalizeFieldAccess(fieldName));
20 |
21 | if (result != null) {
22 | return result;
23 | } else {
24 | return Expr.makeFieldAccess(base, fieldName);
25 | }
26 | }
27 |
28 | @Override
29 | public Expr onRecord(Iterable> fields, int size) {
30 | return NormalizationUtilities.lookup(fields, fieldName);
31 | }
32 |
33 | @Override
34 | public Expr onProjection(Expr base, String[] fieldNames0) {
35 | return Expr.makeFieldAccess(base, fieldName).accept(BetaNormalize.instance);
36 | }
37 |
38 | @Override
39 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) {
40 | if (operator.equals(Operator.PREFER)) {
41 | List> lhsFields = Expr.Util.asRecordLiteral(lhs);
42 | if (lhsFields != null) {
43 | Entry lhsFound = NormalizationUtilities.lookupEntry(lhsFields, fieldName);
44 |
45 | if (lhsFound != null) {
46 | List> singleton = new ArrayList();
47 | singleton.add(lhsFound);
48 |
49 | return Expr.makeFieldAccess(
50 | Expr.makeOperatorApplication(Operator.PREFER, Expr.makeRecordLiteral(singleton), rhs),
51 | fieldName);
52 | } else {
53 | return Expr.makeFieldAccess(rhs, fieldName);
54 | }
55 | } else {
56 | List> rhsFields = Expr.Util.asRecordLiteral(rhs);
57 | if (rhsFields != null) {
58 | Expr rhsFound = NormalizationUtilities.lookup(rhsFields, fieldName);
59 |
60 | if (rhsFound != null) {
61 | return rhsFound;
62 | } else {
63 | return Expr.makeFieldAccess(lhs, fieldName).accept(BetaNormalize.instance);
64 | }
65 | }
66 | }
67 | } else if (operator.equals(Operator.COMBINE)) {
68 | List> lhsFields = Expr.Util.asRecordLiteral(lhs);
69 | if (lhsFields != null) {
70 | Entry lhsFound = NormalizationUtilities.lookupEntry(lhsFields, fieldName);
71 |
72 | if (lhsFound != null) {
73 | List> singleton = new ArrayList<>();
74 | singleton.add(lhsFound);
75 |
76 | return Expr.makeFieldAccess(
77 | Expr.makeOperatorApplication(
78 | Operator.COMBINE, Expr.makeRecordLiteral(singleton), rhs),
79 | fieldName);
80 | } else {
81 | return Expr.makeFieldAccess(rhs, fieldName);
82 | }
83 | } else {
84 | List> rhsFields = Expr.Util.asRecordLiteral(rhs);
85 | if (rhsFields != null) {
86 | Entry rhsFound = NormalizationUtilities.lookupEntry(rhsFields, fieldName);
87 |
88 | if (rhsFound != null) {
89 | List> singleton = new ArrayList<>();
90 | singleton.add(rhsFound);
91 |
92 | return Expr.makeFieldAccess(
93 | Expr.makeOperatorApplication(
94 | Operator.COMBINE, lhs, Expr.makeRecordLiteral(singleton)),
95 | fieldName);
96 | } else {
97 | return Expr.makeFieldAccess(lhs, fieldName).accept(BetaNormalize.instance);
98 | }
99 | }
100 | }
101 | }
102 | return null;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/modules/javagen/src/main/java/org/dhallj/javagen/Code.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.javagen
2 |
3 | case class Code(content: String, defs: Vector[Code] = Vector.empty) {
4 | final private def replace(in: String, target: Int, newName: String): String = {
5 | var last = 0
6 | var next = in.indexOf(Code.marker, last)
7 | val builder = new StringBuilder()
8 |
9 | while (next >= 0) {
10 | builder.append(in.substring(last, next))
11 | val index = in.substring(next + Code.marker.length, next + Code.marker.length + Code.indexLength).toInt
12 | if (index == target) {
13 | builder.append(newName)
14 | } else {
15 | builder.append(in.substring(next, next + Code.marker.length + Code.indexLength))
16 | }
17 |
18 | last = next + Code.marker.length + Code.indexLength
19 | next = in.indexOf(Code.marker, last)
20 | }
21 |
22 | builder.append(in.substring(last))
23 | return builder.toString()
24 | }
25 |
26 | def mapContent(f: String => String): Code = Code(f(Code.makeIdentifier(0)), Vector(this))
27 |
28 | def merge(other: Code)(f: (String, String) => String): Code =
29 | Option(other) match {
30 | case Some(otherValue) => Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1)), Vector(this, other))
31 | case None => this.copy(content = f(content, "null"))
32 | }
33 |
34 | def merge(other0: Code, other1: Code)(f: (String, String, String) => String): Code =
35 | Option(other1) match {
36 | case Some(otherValue) =>
37 | Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1), Code.makeIdentifier(2)), Vector(this, other0, other1))
38 | case None => Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1), "null"), Vector(this, other0))
39 | }
40 |
41 | protected def toFields(known: Map[Code, (String, String)]): (String, Map[Code, (String, String)]) =
42 | known.get(this) match {
43 | case Some((name, impl)) => (name, known)
44 | case None =>
45 | val (newContent, newKnown) = this.defs.zipWithIndex.foldLeft((this.content, known)) {
46 | case ((accContent, accKnown), (code, i)) =>
47 | val (childName, newKnown) = code.toFields(accKnown)
48 |
49 | (this.replace(accContent, i, childName), newKnown)
50 | }
51 |
52 | val nextName = Code.makeFieldName(newKnown.size)
53 | (nextName, newKnown.updated(this, (nextName, newContent)))
54 | }
55 |
56 | def toClassDef(packageName: String, className: String): String = {
57 | val (topLevelFieldName, fields) = toFields(Map.empty)
58 |
59 | val fieldDefs = fields.values.toList
60 | .sortBy(_._1)
61 | .map { case (name, impl) =>
62 | s" private static final Expr $name = $impl;"
63 | }
64 | .mkString("\n")
65 |
66 | s"""package $packageName;
67 | |
68 | |import java.math.BigInteger;
69 | |import java.util.AbstractMap.SimpleImmutableEntry;
70 | |import java.util.ArrayList;
71 | |import java.util.List;
72 | |import java.util.Map.Entry;
73 | |import org.dhallj.core.Expr;
74 | |import org.dhallj.core.Operator;
75 | |
76 | |public final class $className {
77 | |$fieldDefs
78 | |
79 | |public static final Expr instance = $topLevelFieldName;
80 | |}
81 | |""".stripMargin
82 | }
83 | }
84 |
85 | object Code {
86 | private[javagen] val marker: String = "__"
87 | private[javagen] val indexLength: Int = 4
88 | private[javagen] def makeIdentifier(n: Int): String =
89 | String.format(s"%s%0${indexLength}d", marker, Int.box(n))
90 | private[javagen] def makeFieldName(n: Int): String = f"f$n%06d"
91 |
92 | def mergeAll(other: Vector[Code])(f: Vector[String] => String): Code =
93 | Code(f(0.until(other.size).map(makeIdentifier).toVector), other)
94 |
95 | def mergeAll[A](other: Vector[A])(extract: A => Option[Code])(f: Vector[(A, String)] => String): Code = {
96 | var i = -1
97 | val values = other.map { value =>
98 | if (extract(value).isDefined) {
99 | i += 1
100 | (value, makeIdentifier(i))
101 | } else {
102 | (value, "null")
103 | }
104 | }
105 |
106 | Code(f(values), other.flatMap(extract(_)))
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/scalastyle-config.xml:
--------------------------------------------------------------------------------
1 |
2 | Circe Configuration
3 |
4 |
5 | FOR
6 | IF
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | true
77 |
78 |
79 |
80 |
81 | all
82 | .+
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/modules/imports/src/main/scala/org/dhallj/imports/Canonicalization.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.imports
2 |
3 | import java.nio.file.{Path, Paths}
4 |
5 | import cats.implicits._
6 | import cats.effect.Sync
7 | import org.dhallj.core.DhallException.ResolutionFailure
8 | import org.dhallj.imports.ImportContext._
9 |
10 | import scala.collection.JavaConverters._
11 |
12 | object Canonicalization {
13 |
14 | def canonicalize[F[_]](imp: ImportContext)(implicit F: Sync[F]): F[ImportContext] = imp match {
15 | case Remote(uri, headers) => F.delay(Remote(uri.normalize, headers))
16 | case Local(path) => LocalFile[F](path).map(_.canonicalize.toPath).map(Local)
17 | case Classpath(path) => LocalFile[F](path).map(_.canonicalize.toPath).map(Classpath)
18 | case i => F.pure(i)
19 | }
20 |
21 | def canonicalize[F[_]](parent: ImportContext, child: ImportContext)(implicit
22 | F: Sync[F]
23 | ): F[ImportContext] =
24 | parent match {
25 | case Remote(uri, headers) =>
26 | child match {
27 | //A transitive relative import is parsed as local but is resolved as a remote import
28 | // eg https://github.com/dhall-lang/dhall-lang/blob/master/Prelude/Integer/add has a local import but we still
29 | //need to resolve this as a remote import
30 | //Also note that if the path is absolute then this violates referential sanity but we handle that elsewhere
31 | case Local(path) =>
32 | if (path.isAbsolute) canonicalize(child)
33 | else canonicalize(Remote(uri.resolve(path.toString), headers))
34 | case _ => canonicalize(child)
35 | }
36 | case Local(path) =>
37 | child match {
38 | case Local(path2) =>
39 | for {
40 | parent <- LocalFile[F](path)
41 | c <- LocalFile[F](path2)
42 | } yield Local(parent.chain(c).canonicalize.toPath)
43 | case _ => canonicalize(child)
44 | }
45 | //TODO - determine semantics of classpath imports
46 | case Classpath(path) =>
47 | child match {
48 | //Also note that if the path is absolute then this violates referential sanity but we handle that elsewhere
49 | case Local(path2) =>
50 | for {
51 | parent <- LocalFile[F](path)
52 | c <- LocalFile[F](path2)
53 | } yield Classpath(parent.chain(c).canonicalize.toPath)
54 | case _ => canonicalize(child)
55 | }
56 | case _ => canonicalize(child)
57 | }
58 |
59 | private case class LocalFile(dirs: LocalDirs, filename: String) {
60 | def toPath: Path = {
61 | def toPath(l: List[String]) = "/" + l.intercalate("/")
62 |
63 | val s: String = dirs.ds match {
64 | case Nil => ""
65 | case l @ (h :: t) =>
66 | h match {
67 | case h if h == "." || h == ".." || h == "~" => s"$h${toPath(t)}"
68 | case _ => toPath(l)
69 | }
70 | }
71 | Paths.get(s"$s/$filename")
72 | }
73 |
74 | def chain(other: LocalFile): LocalFile = LocalFile.chain(this, other)
75 |
76 | def canonicalize: LocalFile = LocalFile.canonicalize(this)
77 | }
78 |
79 | private object LocalFile {
80 | def apply[F[_]](path: Path)(implicit F: Sync[F]): F[LocalFile] =
81 | path.iterator().asScala.toList.map(_.toString) match {
82 | case Nil => F.raiseError(new ResolutionFailure("This shouldn't happen - / can't import a dhall expression"))
83 | case l => F.pure(LocalFile(LocalDirs(l.take(l.length - 1)), l.last))
84 | }
85 |
86 | def canonicalize(f: LocalFile): LocalFile =
87 | LocalFile(f.dirs.canonicalize, f.filename.stripPrefix("\"").stripSuffix("\""))
88 |
89 | def chain(lhs: LocalFile, rhs: LocalFile): LocalFile = LocalFile(LocalDirs.chain(lhs.dirs, rhs.dirs), rhs.filename)
90 | }
91 |
92 | private case class LocalDirs(ds: List[String]) {
93 | def isRelative: Boolean = ds.nonEmpty && (ds.head == "." || ds.head == "..")
94 |
95 | def canonicalize: LocalDirs = LocalDirs.canonicalize(this)
96 |
97 | def chain(other: LocalDirs): LocalDirs = LocalDirs.chain(this, other)
98 | }
99 |
100 | private object LocalDirs {
101 | def chain(lhs: LocalDirs, rhs: LocalDirs): LocalDirs = if (rhs.isRelative) LocalDirs(lhs.ds ++ rhs.ds) else rhs
102 |
103 | def canonicalize(d: LocalDirs): LocalDirs = d.ds match {
104 | case Nil => d
105 | case l => LocalDirs(canonicalize(l.map(_.stripPrefix("\"").stripSuffix("\""))))
106 | }
107 |
108 | def canonicalize(l: List[String]): List[String] =
109 | l.tail
110 | .foldLeft(List(l.head)) { (acc, next) =>
111 | next match {
112 | case "." => acc
113 | case ".." => acc.tail
114 | case o => o :: acc
115 | }
116 | }
117 | .reverse
118 |
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/typechecking/BuiltInTypes.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.typechecking;
2 |
3 | import java.util.AbstractMap.SimpleImmutableEntry;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 | import java.util.Map.Entry;
7 | import org.dhallj.core.Expr;
8 | import org.dhallj.core.Expr.Constants;
9 |
10 | final class BuiltInTypes {
11 | static final Expr getType(String name) {
12 | return mappings.get(name);
13 | }
14 |
15 | private static final int SIZE = 34;
16 | private static final Map mappings = new HashMap<>(SIZE);
17 |
18 | static {
19 | Expr typeToType = Expr.makePi(Constants.TYPE, Constants.TYPE);
20 | Expr naturalToBool = Expr.makePi(Constants.NATURAL, Constants.BOOL);
21 | Expr naturalToNatural = Expr.makePi(Constants.NATURAL, Constants.NATURAL);
22 | Expr _natural = Expr.makeIdentifier("natural");
23 | Expr naturalType =
24 | Expr.makePi(
25 | "natural",
26 | Constants.TYPE,
27 | Expr.makePi(
28 | "succ", Expr.makePi(_natural, _natural), Expr.makePi("zero", _natural, _natural)));
29 | Expr _a = Expr.makeIdentifier("a");
30 | Expr listA = Expr.makeApplication(Constants.LIST, _a);
31 | Expr optionalA = Expr.makeApplication(Constants.OPTIONAL, _a);
32 | Expr listAToOptionalA = Expr.makePi("a", Expr.Constants.TYPE, Expr.makePi(listA, optionalA));
33 |
34 | Expr _list = Expr.makeIdentifier("list");
35 | Expr listType =
36 | Expr.makePi(
37 | "list",
38 | Constants.TYPE,
39 | Expr.makePi(
40 | "cons",
41 | Expr.makePi(_a, Expr.makePi(_list, _list)),
42 | Expr.makePi("nil", _list, _list)));
43 |
44 | mappings.put("Kind", Constants.SORT);
45 | mappings.put("Type", Constants.KIND);
46 | mappings.put("Bool", Constants.TYPE);
47 | mappings.put("True", Constants.BOOL);
48 | mappings.put("False", Constants.BOOL);
49 | mappings.put("Natural", Constants.TYPE);
50 | mappings.put("Integer", Constants.TYPE);
51 | mappings.put("Text", Constants.TYPE);
52 | mappings.put("Double", Constants.TYPE);
53 | mappings.put("List", typeToType);
54 | mappings.put("Optional", typeToType);
55 | mappings.put(
56 | "None",
57 | Expr.makePi(
58 | "A",
59 | Constants.TYPE,
60 | Expr.makeApplication(Constants.OPTIONAL, Expr.makeIdentifier("A"))));
61 |
62 | mappings.put(
63 | "Text/replace",
64 | Expr.makePi(
65 | "needle",
66 | Constants.TEXT,
67 | Expr.makePi(
68 | "replacement",
69 | Constants.TEXT,
70 | Expr.makePi("haystack", Constants.TEXT, Constants.TEXT))));
71 | mappings.put("Text/show", Expr.makePi(Constants.TEXT, Constants.TEXT));
72 |
73 | mappings.put("Natural/build", Expr.makePi(naturalType, Constants.NATURAL));
74 | mappings.put("Natural/fold", Expr.makePi(Constants.NATURAL, naturalType));
75 | mappings.put("Natural/isZero", naturalToBool);
76 | mappings.put("Natural/even", naturalToBool);
77 | mappings.put("Natural/odd", naturalToBool);
78 | mappings.put("Natural/toInteger", Expr.makePi(Constants.NATURAL, Constants.INTEGER));
79 | mappings.put("Natural/show", Expr.makePi(Constants.NATURAL, Constants.TEXT));
80 | mappings.put("Natural/subtract", Expr.makePi(Constants.NATURAL, naturalToNatural));
81 |
82 | mappings.put("Integer/show", Expr.makePi(Constants.INTEGER, Constants.TEXT));
83 | mappings.put("Integer/toDouble", Expr.makePi(Constants.INTEGER, Constants.DOUBLE));
84 | mappings.put("Integer/negate", Expr.makePi(Constants.INTEGER, Constants.INTEGER));
85 | mappings.put("Integer/clamp", Expr.makePi(Constants.INTEGER, Constants.NATURAL));
86 |
87 | mappings.put("Double/show", Expr.makePi(Constants.DOUBLE, Constants.TEXT));
88 |
89 | mappings.put("List/build", Expr.makePi("a", Constants.TYPE, Expr.makePi(listType, listA)));
90 | mappings.put("List/fold", Expr.makePi("a", Constants.TYPE, Expr.makePi(listA, listType)));
91 | mappings.put(
92 | "List/length",
93 | Expr.makePi("a", Expr.Constants.TYPE, Expr.makePi(listA, Constants.NATURAL)));
94 | mappings.put("List/head", listAToOptionalA);
95 | mappings.put("List/last", listAToOptionalA);
96 |
97 | Entry[] indexedRecordFields = {
98 | new SimpleImmutableEntry<>("index", Constants.NATURAL),
99 | new SimpleImmutableEntry<>("value", _a)
100 | };
101 |
102 | mappings.put(
103 | "List/indexed",
104 | Expr.makePi(
105 | "a",
106 | Constants.TYPE,
107 | Expr.makePi(
108 | listA,
109 | Expr.makeApplication(Constants.LIST, Expr.makeRecordType(indexedRecordFields)))));
110 | mappings.put("List/reverse", Expr.makePi("a", Constants.TYPE, Expr.makePi(listA, listA)));
111 |
112 | mappings.put("Some", Constants.SOME);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalize.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.normalization;
2 |
3 | import java.math.BigInteger;
4 | import java.net.URI;
5 | import java.nio.file.Path;
6 | import java.util.Collections;
7 | import java.util.List;
8 | import java.util.Map.Entry;
9 | import java.util.Set;
10 | import java.util.TreeSet;
11 | import org.dhallj.core.Expr;
12 | import org.dhallj.core.Operator;
13 | import org.dhallj.core.Source;
14 | import org.dhallj.core.Visitor;
15 |
16 | /**
17 | * Performs beta normalization.
18 | *
19 | * This is a stateless visitor intended for use as a singleton.
20 | */
21 | public final class BetaNormalize extends Visitor.NoPrepareEvents {
22 | public static final Visitor instance = new BetaNormalize();
23 |
24 | public void bind(String name, Expr type) {}
25 |
26 | public Expr onNote(Expr base, Source source) {
27 | return base;
28 | }
29 |
30 | public Expr onNatural(Expr self, BigInteger value) {
31 | return self;
32 | }
33 |
34 | public Expr onInteger(Expr self, BigInteger value) {
35 | return self;
36 | }
37 |
38 | public Expr onDouble(Expr self, double value) {
39 | return self;
40 | }
41 |
42 | public Expr onBuiltIn(Expr self, String name) {
43 | return self;
44 | }
45 |
46 | public Expr onIdentifier(Expr self, String name, long index) {
47 | return self;
48 | }
49 |
50 | public Expr onLambda(String name, Expr type, Expr result) {
51 | return Expr.makeLambda(name, type, result);
52 | }
53 |
54 | public Expr onPi(String name, Expr type, Expr result) {
55 | return Expr.makePi(name, type, result);
56 | }
57 |
58 | public Expr onLet(List> bindings, Expr body) {
59 | Expr result = body;
60 |
61 | for (int i = bindings.size() - 1; i >= 0; i--) {
62 | Expr.LetBinding binding = bindings.get(i);
63 | String name = binding.getName();
64 |
65 | result = result.substitute(name, binding.getValue());
66 | }
67 |
68 | return result.accept(this);
69 | }
70 |
71 | public Expr onText(String[] parts, List interpolated) {
72 | return BetaNormalizeTextLiteral.apply(parts, interpolated);
73 | }
74 |
75 | public Expr onNonEmptyList(List values) {
76 | return Expr.makeNonEmptyListLiteral(values);
77 | }
78 |
79 | public Expr onEmptyList(Expr type) {
80 | return Expr.makeEmptyListLiteral(type);
81 | }
82 |
83 | public Expr onRecord(List> fields) {
84 | Collections.sort(fields, NormalizationUtilities.entryComparator);
85 | return Expr.makeRecordLiteral(fields);
86 | }
87 |
88 | public Expr onRecordType(List> fields) {
89 | Collections.sort(fields, NormalizationUtilities.entryComparator);
90 | return Expr.makeRecordType(fields);
91 | }
92 |
93 | public Expr onUnionType(List> fields) {
94 | Collections.sort(fields, NormalizationUtilities.entryComparator);
95 | return Expr.makeUnionType(fields);
96 | }
97 |
98 | public Expr onFieldAccess(Expr base, String fieldName) {
99 | return BetaNormalizeFieldAccess.apply(base, fieldName);
100 | }
101 |
102 | public Expr onProjection(Expr base, String[] fieldNames) {
103 | return BetaNormalizeProjection.apply(base, fieldNames);
104 | }
105 |
106 | public Expr onProjectionByType(Expr base, Expr arg) {
107 | List> argAsRecordType = Expr.Util.asRecordType(arg);
108 | Set keys = new TreeSet<>();
109 | for (Entry entry : argAsRecordType) {
110 | keys.add(entry.getKey());
111 | }
112 |
113 | return Expr.makeProjection(base, keys.toArray(new String[keys.size()])).accept(this);
114 | }
115 |
116 | public Expr onApplication(Expr base, List args) {
117 | return BetaNormalizeApplication.apply(base, args);
118 | }
119 |
120 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) {
121 | return BetaNormalizeOperatorApplication.apply(operator, lhs, rhs);
122 | }
123 |
124 | public Expr onIf(Expr predicate, Expr thenValue, Expr elseValue) {
125 | return BetaNormalizeIf.apply(predicate, thenValue, elseValue);
126 | }
127 |
128 | public Expr onAnnotated(Expr base, Expr type) {
129 | return base;
130 | }
131 |
132 | public Expr onAssert(Expr type) {
133 | return Expr.makeAssert(type);
134 | }
135 |
136 | public Expr onMerge(Expr handlers, Expr union, Expr type) {
137 | return BetaNormalizeMerge.apply(handlers, union, type);
138 | }
139 |
140 | public Expr onToMap(Expr base, Expr type) {
141 | return BetaNormalizeToMap.apply(base, type);
142 | }
143 |
144 | public Expr onWith(Expr base, String[] path, Expr value) {
145 | return BetaNormalizeWith.apply(base, path, value);
146 | }
147 |
148 | public Expr onMissingImport(Expr.ImportMode mode, byte[] hash) {
149 | return Expr.makeMissingImport(mode, hash);
150 | }
151 |
152 | public Expr onEnvImport(String value, Expr.ImportMode mode, byte[] hash) {
153 | return Expr.makeEnvImport(value, mode, hash);
154 | }
155 |
156 | public Expr onLocalImport(Path path, Expr.ImportMode mode, byte[] hash) {
157 | return Expr.makeLocalImport(path, mode, hash);
158 | }
159 |
160 | @Override
161 | public Expr onClasspathImport(Path path, Expr.ImportMode mode, byte[] hash) {
162 | return Expr.makeClasspathImport(path, mode, hash);
163 | }
164 |
165 | public Expr onRemoteImport(URI url, Expr using, Expr.ImportMode mode, byte[] hash) {
166 | return Expr.makeRemoteImport(url, using, mode, hash);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/tests/src/main/scala/org/dhallj/tests/acceptance/AcceptanceSuccessSuite.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.tests.acceptance
2 |
3 | import cats.effect.{ContextShift, IO}
4 | import java.nio.file.{Path, Paths}
5 |
6 | import org.dhallj.core.Expr
7 | import org.dhallj.core.binary.Decode.decode
8 | import org.dhallj.parser.DhallParser
9 |
10 | import org.dhallj.imports.{ImportCache, Resolver}
11 | import org.http4s.client._
12 | import org.http4s.client.blaze._
13 |
14 | import scala.concurrent.ExecutionContext
15 |
16 | trait SuccessSuite[A, B] extends AcceptanceSuite {
17 | self: Input[A] =>
18 |
19 | def makeExpectedFilename(input: String): String
20 | final private def makeExpectedPath(inputPath: Path): Path =
21 | inputPath.resolveSibling(makeExpectedFilename(inputPath.getFileName.toString))
22 |
23 | def transform(input: A): B
24 | def loadExpected(input: Array[Byte]): B
25 | def compare(result: B, expected: B): Boolean
26 |
27 | def testPairs: List[(String, Path, (String, B))] = testInputs.map { case (name, path) =>
28 | (name, path, (readString(path), loadExpected(readBytes(makeExpectedPath(path)))))
29 | }
30 |
31 | testPairs.foreach { case (name, path, (input, expected)) =>
32 | test(name) {
33 | assert(compare(clue(transform(parseInput(path.toString, clue(input)))), clue(expected)))
34 | }
35 | }
36 | }
37 |
38 | trait Input[A] {
39 | def parseInput(path: String, input: String): A
40 |
41 | }
42 |
43 | trait ParsingInput extends Input[Expr] {
44 |
45 | override def parseInput(path: String, input: String): Expr = DhallParser.parse(input)
46 |
47 | }
48 |
49 | trait CachedResolvingInput extends Input[Expr] {
50 |
51 | override def parseInput(path: String, input: String): Expr = {
52 | //TODO this should only be for import tests (I think)
53 | val parsed = DhallParser.parse(s"./$path")
54 |
55 | if (parsed.isResolved) parsed
56 | else {
57 | implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
58 | BlazeClientBuilder[IO](ExecutionContext.global).resource
59 | .use { client =>
60 | implicit val c: Client[IO] = client
61 | Resolver.resolve[IO](parsed)
62 | }
63 | .unsafeRunSync()
64 | }
65 | }
66 |
67 | }
68 |
69 | trait ResolvingInput extends Input[Expr] {
70 | def parseInput(path: String, input: String): Expr = {
71 | //TODO this should only be for import tests (I think)
72 | val parsed = DhallParser.parse(s"./$path")
73 |
74 | if (parsed.isResolved) parsed
75 | else {
76 | implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
77 | BlazeClientBuilder[IO](ExecutionContext.global).resource
78 | .use { client =>
79 | implicit val c: Client[IO] = client
80 | Resolver.resolve[IO](new ImportCache.NoopImportCache[IO], new ImportCache.NoopImportCache[IO])(parsed)
81 | }
82 | .unsafeRunSync()
83 | }
84 | }
85 | }
86 |
87 | abstract class ExprOperationAcceptanceSuite(transformation: Expr => Expr) extends SuccessSuite[Expr, Expr] {
88 | self: Input[Expr] =>
89 |
90 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.dhall"
91 |
92 | def transform(input: Expr): Expr = transformation(input)
93 |
94 | def loadExpected(input: Array[Byte]): Expr = DhallParser.parse(new String(input))
95 | def compare(result: Expr, expected: Expr): Boolean = result.sameStructure(expected) && result.equivalent(expected)
96 | }
97 |
98 | class ParsingTypeCheckingSuite(val base: String, override val recurse: Boolean = false)
99 | extends ExprOperationAcceptanceSuite(Expr.Util.typeCheck(_))
100 | with ParsingInput
101 | class TypeCheckingSuite(val base: String, override val recurse: Boolean = false)
102 | extends ExprOperationAcceptanceSuite(Expr.Util.typeCheck(_))
103 | with ResolvingInput
104 | class AlphaNormalizationSuite(val base: String) extends ExprOperationAcceptanceSuite(_.alphaNormalize) with ParsingInput
105 | class NormalizationSuite(val base: String, override val recurse: Boolean = false)
106 | extends ExprOperationAcceptanceSuite(_.normalize)
107 | with CachedResolvingInput
108 | class NormalizationUSuite(val base: String) extends ExprOperationAcceptanceSuite(_.normalize) with ParsingInput
109 |
110 | class HashingSuite(val base: String, override val recurse: Boolean = false)
111 | extends SuccessSuite[Expr, String]
112 | with ResolvingInput {
113 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.hash"
114 |
115 | def transform(input: Expr): String = input.normalize.alphaNormalize.hash
116 | def loadExpected(input: Array[Byte]): String = new String(input).trim.drop(7)
117 | def compare(result: String, expected: String): Boolean = result == expected
118 | }
119 |
120 | class ParsingSuite(val base: String) extends SuccessSuite[Expr, Array[Byte]] with ParsingInput {
121 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.dhallb"
122 |
123 | def transform(input: Expr): Array[Byte] = input.getEncodedBytes
124 | def loadExpected(input: Array[Byte]): Array[Byte] = input
125 | def compare(result: Array[Byte], expected: Array[Byte]): Boolean = result.sameElements(expected)
126 | }
127 |
128 | class BinaryDecodingSuite(val base: String) extends SuccessSuite[Expr, Expr] with ParsingInput {
129 | def makeExpectedFilename(input: String): String = input.dropRight(8) + "B.dhall"
130 |
131 | override def isInputFileName(fileName: String): Boolean = fileName.endsWith("A.dhallb")
132 |
133 | override def parseInput(path: String, input: String): Expr =
134 | decode(readBytes(Paths.get(path)))
135 |
136 | def transform(input: Expr): Expr = input
137 | def loadExpected(input: Array[Byte]): Expr = DhallParser.parse(new String(input))
138 | def compare(result: Expr, expected: Expr): Boolean = result.sameStructure(expected) && result.equivalent(expected)
139 | }
140 |
--------------------------------------------------------------------------------
/modules/scala-codec/src/main/scala/org/dhallj/codec/Encoder.scala:
--------------------------------------------------------------------------------
1 | package org.dhallj.codec
2 |
3 | import org.dhallj.core.Expr
4 | import org.dhallj.ast._
5 |
6 | trait Encoder[A] { self =>
7 | def encode(value: A, target: Option[Expr]): Expr
8 | def dhallType(value: Option[A], target: Option[Expr]): Expr
9 |
10 | final def encode(value: A): Expr = encode(value, None)
11 | final def isExactType(typeExpr: Expr): Boolean = typeExpr == dhallType(None, Some(typeExpr))
12 |
13 | def contramap[B](f: B => A): Encoder[B] = new Encoder[B] {
14 | def encode(value: B, target: Option[Expr]): Expr = self.encode(f(value), target)
15 | def dhallType(value: Option[B], target: Option[Expr]): Expr =
16 | self.dhallType(value.map(f), target)
17 | }
18 | }
19 |
20 | object Encoder {
21 | def apply[A](implicit A: Encoder[A]): Encoder[A] = A
22 |
23 | implicit val encodeLong: Encoder[Long] = new Encoder[Long] {
24 | def encode(value: Long, target: Option[Expr]): Expr = {
25 | val asBigInt = BigInt(value)
26 |
27 | target match {
28 | case Some(Expr.Constants.INTEGER) => IntegerLiteral(asBigInt)
29 | case _ => NaturalLiteral(asBigInt).getOrElse(IntegerLiteral(asBigInt))
30 | }
31 | }
32 |
33 | def dhallType(value: Option[Long], target: Option[Expr]): Expr = target match {
34 | case Some(Expr.Constants.INTEGER) => Expr.Constants.INTEGER
35 | case _ =>
36 | value match {
37 | case Some(v) if v < 0 => Expr.Constants.INTEGER
38 | case _ => Expr.Constants.NATURAL
39 | }
40 | }
41 | }
42 |
43 | implicit val encodeInt: Encoder[Int] = encodeLong.contramap(_.toLong)
44 |
45 | implicit val encodeBigInt: Encoder[BigInt] = new Encoder[BigInt] {
46 | def encode(value: BigInt, target: Option[Expr]): Expr = target match {
47 | case Some(Expr.Constants.INTEGER) => IntegerLiteral(value)
48 | case _ => NaturalLiteral(value).getOrElse(IntegerLiteral(value))
49 | }
50 |
51 | def dhallType(value: Option[BigInt], target: Option[Expr]): Expr = target match {
52 | case Some(Expr.Constants.INTEGER) => Expr.Constants.INTEGER
53 | case _ =>
54 | value match {
55 | case Some(v) if v < 0 => Expr.Constants.INTEGER
56 | case _ => Expr.Constants.NATURAL
57 | }
58 | }
59 | }
60 |
61 | implicit val encodeDouble: Encoder[Double] = new Encoder[Double] {
62 | def encode(value: Double, target: Option[Expr]): Expr = DoubleLiteral(value)
63 | def dhallType(value: Option[Double], target: Option[Expr]): Expr = Expr.Constants.DOUBLE
64 | }
65 |
66 | implicit val encodeString: Encoder[String] = new Encoder[String] {
67 | def encode(value: String, target: Option[Expr]): Expr = TextLiteral(value)
68 | def dhallType(value: Option[String], target: Option[Expr]): Expr = Expr.Constants.TEXT
69 | }
70 |
71 | implicit val encodeBoolean: Encoder[Boolean] = new Encoder[Boolean] {
72 | def encode(value: Boolean, target: Option[Expr]): Expr = BoolLiteral(value)
73 | def dhallType(value: Option[Boolean], target: Option[Expr]): Expr = Expr.Constants.BOOL
74 | }
75 |
76 | implicit def encodeOption[A: Encoder]: Encoder[Option[A]] = new Encoder[Option[A]] {
77 | def encode(value: Option[A], target: Option[Expr]): Expr = target match {
78 | case Some(Application(Expr.Constants.OPTIONAL, elementType)) =>
79 | value match {
80 | case Some(a) => Application(Expr.Constants.SOME, Encoder[A].encode(a, Some(elementType)))
81 | case None =>
82 | Application(Expr.Constants.NONE, Encoder[A].dhallType(None, Some(elementType)))
83 | }
84 | case _ =>
85 | value match {
86 | case Some(a) => Application(Expr.Constants.SOME, Encoder[A].encode(a))
87 | case None =>
88 | Application(Expr.Constants.NONE, Encoder[A].dhallType(None, None))
89 | }
90 | }
91 |
92 | def dhallType(value: Option[Option[A]], target: Option[Expr]): Expr = {
93 | val targetElementType = target.flatMap {
94 | case Application(Expr.Constants.OPTIONAL, elementType) => Some(elementType)
95 | case _ => None
96 | }
97 |
98 | Application(Expr.Constants.OPTIONAL, Encoder[A].dhallType(value.flatten, targetElementType))
99 | }
100 | }
101 |
102 | implicit def encodeVector[A: Encoder]: Encoder[Vector[A]] = new Encoder[Vector[A]] {
103 | def encode(value: Vector[A], target: Option[Expr]): Expr = {
104 | val tpe = dhallElementType(Some(value), target)
105 |
106 | value match {
107 | case Vector() => EmptyListLiteral(Application(Expr.Constants.LIST, tpe))
108 | case h +: t =>
109 | NonEmptyListLiteral(
110 | Encoder[A].encode(h, Some(tpe)),
111 | t.map(Encoder[A].encode(_, Some(tpe)))
112 | )
113 | }
114 | }
115 |
116 | private def dhallElementType(value: Option[Vector[A]], target: Option[Expr]): Expr = {
117 | val targetElementType = target.flatMap {
118 | case Application(Expr.Constants.LIST, elementType) => Some(elementType)
119 | case _ => None
120 | }
121 |
122 | value match {
123 | case None => Encoder[A].dhallType(None, targetElementType)
124 | case Some(Vector()) => Encoder[A].dhallType(None, targetElementType)
125 | case Some(h +: t) =>
126 | t.foldLeft(Encoder[A].dhallType(Some(h), targetElementType)) { case (acc, a) =>
127 | Encoder[A].dhallType(Some(a), Some(acc))
128 | }
129 | }
130 | }
131 |
132 | def dhallType(value: Option[Vector[A]], target: Option[Expr]): Expr =
133 | Application(Expr.Constants.LIST, dhallElementType(value, target))
134 | }
135 |
136 | implicit def encodeList[A: Encoder]: Encoder[List[A]] = encodeVector[A].contramap(_.toVector)
137 | }
138 |
--------------------------------------------------------------------------------
/modules/core/src/main/java/org/dhallj/core/converters/JsonConverter.java:
--------------------------------------------------------------------------------
1 | package org.dhallj.core.converters;
2 |
3 | import java.math.BigInteger;
4 | import java.util.List;
5 | import java.util.Map.Entry;
6 | import org.dhallj.core.Expr;
7 | import org.dhallj.core.Visitor;
8 |
9 | public final class JsonConverter extends Visitor.Constant {
10 | private final JsonHandler handler;
11 | private final boolean escapeStrings;
12 |
13 | public JsonConverter(JsonHandler handler, boolean escapeStrings) {
14 | super(false);
15 | this.handler = handler;
16 | this.escapeStrings = escapeStrings;
17 | }
18 |
19 | public JsonConverter(JsonHandler handler) {
20 | this(handler, true);
21 | }
22 |
23 | public static final String toCompactString(Expr expr) {
24 | JsonHandler.CompactStringPrinter handler = new JsonHandler.CompactStringPrinter();
25 | boolean isConverted = expr.accept(new JsonConverter(handler));
26 | if (isConverted) {
27 | return handler.toString();
28 | } else {
29 | return null;
30 | }
31 | }
32 |
33 | private static final String escape(String input) {
34 | StringBuilder builder = new StringBuilder();
35 |
36 | for (int i = 0; i < input.length(); i++) {
37 | char c = input.charAt(i);
38 |
39 | if (c == '\\') {
40 | char next = input.charAt(++i);
41 |
42 | if (next == '"') {
43 | builder.append("\\\"");
44 | } else if (next == '$') {
45 | builder.append("$");
46 | } else {
47 | builder.append(c);
48 | builder.append(next);
49 | }
50 | } else if (c == '"') {
51 | builder.append("\\\"");
52 | } else {
53 | builder.append(c);
54 | }
55 | }
56 |
57 | return builder.toString();
58 | }
59 |
60 | @Override
61 | public boolean sortFields() {
62 | return false;
63 | }
64 |
65 | @Override
66 | public boolean flattenToMapLists() {
67 | return true;
68 | }
69 |
70 | @Override
71 | public Boolean onNatural(Expr self, BigInteger value) {
72 | this.handler.onNumber(value);
73 | return true;
74 | }
75 |
76 | @Override
77 | public Boolean onInteger(Expr self, BigInteger value) {
78 | this.handler.onNumber(value);
79 | return true;
80 | }
81 |
82 | @Override
83 | public Boolean onDouble(Expr self, double value) {
84 | this.handler.onDouble(value);
85 | return true;
86 | }
87 |
88 | @Override
89 | public Boolean onBuiltIn(Expr self, String name) {
90 | if (name.equals("True")) {
91 | this.handler.onBoolean(true);
92 | return true;
93 | } else if (name.equals("False")) {
94 | this.handler.onBoolean(false);
95 | return true;
96 | } else {
97 | return false;
98 | }
99 | }
100 |
101 | @Override
102 | public Boolean onText(String[] parts, List interpolated) {
103 | if (parts.length == 1) {
104 | if (this.escapeStrings) {
105 | this.handler.onString(escape(parts[0]));
106 | } else {
107 | this.handler.onString(parts[0]);
108 | }
109 | return true;
110 | } else {
111 | return false;
112 | }
113 | }
114 |
115 | @Override
116 | public boolean prepareNonEmptyList(int size) {
117 | this.handler.onArrayStart();
118 | return true;
119 | }
120 |
121 | @Override
122 | public boolean prepareNonEmptyListElement(int index) {
123 | if (index > 0) {
124 | this.handler.onArrayElementGap();
125 | }
126 | return true;
127 | }
128 |
129 | @Override
130 | public Boolean onNonEmptyList(List values) {
131 | for (boolean value : values) {
132 | if (!value) {
133 | return false;
134 | }
135 | }
136 | this.handler.onArrayEnd();
137 | return true;
138 | }
139 |
140 | @Override
141 | public Boolean onEmptyList(Boolean type) {
142 | this.handler.onArrayStart();
143 | this.handler.onArrayEnd();
144 | return true;
145 | }
146 |
147 | @Override
148 | public boolean prepareRecord(int size) {
149 | this.handler.onObjectStart();
150 | return true;
151 | }
152 |
153 | @Override
154 | public boolean prepareRecordField(String name, Expr type, int index) {
155 | if (index > 0) {
156 | this.handler.onObjectFieldGap();
157 | }
158 | if (this.escapeStrings) {
159 | this.handler.onObjectField(escape(name));
160 | } else {
161 | this.handler.onObjectField(name);
162 | }
163 | return true;
164 | }
165 |
166 | @Override
167 | public Boolean onRecord(final List> fields) {
168 | for (Entry field : fields) {
169 | if (!field.getValue()) {
170 | return false;
171 | }
172 | }
173 | this.handler.onObjectEnd();
174 | return true;
175 | }
176 |
177 | @Override
178 | public boolean prepareFieldAccess(Expr base, String fieldName) {
179 | List> asUnion = Expr.Util.asUnionType(base);
180 |
181 | if (asUnion != null) {
182 | for (Entry field : asUnion) {
183 | if (field.getKey().equals(fieldName) && field.getValue() == null) {
184 | this.handler.onString(escape(fieldName));
185 | return false;
186 | }
187 | }
188 | }
189 | return true;
190 | }
191 |
192 | @Override
193 | public Boolean onFieldAccess(Boolean base, String fieldName) {
194 | return base == null || base;
195 | }
196 |
197 | @Override
198 | public boolean prepareApplication(Expr base, int size) {
199 | String asBuiltIn = Expr.Util.asBuiltIn(base);
200 |
201 | if (asBuiltIn != null && size == 1) {
202 | if (asBuiltIn.equals("Some")) {
203 | return false;
204 | } else if (asBuiltIn.equals("None")) {
205 | this.handler.onNull();
206 | return false;
207 | }
208 | } else {
209 | Entry asFieldAccess = Expr.Util.asFieldAccess(base);
210 |
211 | if (asFieldAccess != null) {
212 | List> asUnion = Expr.Util.asUnionType(asFieldAccess.getKey());
213 |
214 | if (asUnion != null) {
215 | for (Entry field : asUnion) {
216 | if (field.getKey().equals(asFieldAccess.getValue()) && field.getValue() != null) {
217 | return false;
218 | }
219 | }
220 | }
221 | }
222 | }
223 |
224 | return true;
225 | }
226 |
227 | @Override
228 | public Boolean onApplication(Boolean base, List args) {
229 | return base == null || base;
230 | }
231 | }
232 |
--------------------------------------------------------------------------------