results) {
+
+ public boolean ok() {
+ return results.stream().allMatch(ExampleResult::ok);
+ }
+ }
+}
diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java
similarity index 88%
rename from planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java
rename to planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java
index 65385adc91..2014e9b9af 100644
--- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaSpecification.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/validator/SchemaSpecification.java
@@ -1,17 +1,21 @@
-package com.onthegomap.planetiler.custommap.validator;
+package com.onthegomap.planetiler.validator;
import static com.onthegomap.planetiler.config.PlanetilerConfig.MAX_MAXZOOM;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.onthegomap.planetiler.custommap.YAML;
import com.onthegomap.planetiler.geo.GeometryType;
+import com.onthegomap.planetiler.util.YAML;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
-/** A model of example input source features and expected output vector tile features that a schema should produce. */
+/**
+ * A model of example input source features and expected output vector tile features that a schema should produce.
+ *
+ * Executed by a subclass of {@link BaseSchemaValidator}.
+ */
@JsonIgnoreProperties(ignoreUnknown = true)
public record SchemaSpecification(List examples) {
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
index d1224780f3..ade78a6639 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
@@ -204,18 +204,6 @@ private PlanetilerResults runWithReaderFeaturesProfile(
);
}
- private PlanetilerResults runWithOsmElements(
- Map args,
- List features,
- BiConsumer profileFunction
- ) throws Exception {
- return run(
- args,
- (featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features),
- TestProfile.processSourceFeatures(profileFunction)
- );
- }
-
private PlanetilerResults runWithOsmElements(
Map args,
List features,
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
index 5064b1ff65..876ad03ffa 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/TestUtils.java
@@ -7,6 +7,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -26,6 +27,8 @@
import com.onthegomap.planetiler.mbtiles.Verify;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.stats.Stats;
+import com.onthegomap.planetiler.validator.BaseSchemaValidator;
+import com.onthegomap.planetiler.validator.SchemaSpecification;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -47,7 +50,9 @@
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.commons.lang3.reflect.FieldUtils;
+import org.junit.jupiter.api.DynamicNode;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
@@ -340,6 +345,20 @@ public static Path extractPathToResource(Path tempDir, String resource, String l
return path;
}
+ public static Stream validateProfile(Profile profile, String spec) {
+ return validateProfile(profile, SchemaSpecification.load(spec));
+ }
+
+ public static Stream validateProfile(Profile profile, SchemaSpecification spec) {
+ var result = BaseSchemaValidator.validate(profile, spec, PlanetilerConfig.defaults());
+ return result.results().stream().map(test -> dynamicTest(test.example().name(), () -> {
+ var issues = test.issues().get();
+ if (!issues.isEmpty()) {
+ fail("Failed with " + issues.size() + " issues:\n" + String.join("\n", issues));
+ }
+ }));
+ }
+
public interface GeometryComparision {
Geometry geom();
@@ -632,7 +651,7 @@ public static void assertMinFeatureCount(Mbtiles db, String layer, int zoom, Map
try {
int num = Verify.getNumFeatures(db, layer, zoom, attrs, envelope, clazz);
- assertTrue(expected < num,
+ assertTrue(expected <= num,
"z%d features in %s, expected at least %d got %d".formatted(zoom, layer, expected, num));
} catch (GeometryException e) {
fail(e);
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java
new file mode 100644
index 0000000000..5ce9bd1ced
--- /dev/null
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/validator/BaseSchemaValidatorTest.java
@@ -0,0 +1,204 @@
+package com.onthegomap.planetiler.validator;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.onthegomap.planetiler.FeatureCollector;
+import com.onthegomap.planetiler.Profile;
+import com.onthegomap.planetiler.TestUtils;
+import com.onthegomap.planetiler.config.Arguments;
+import com.onthegomap.planetiler.config.PlanetilerConfig;
+import com.onthegomap.planetiler.reader.SourceFeature;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicNode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class BaseSchemaValidatorTest {
+
+ private final String goodSpecString = """
+ examples:
+ - name: test output
+ input:
+ source: osm
+ geometry: polygon
+ tags:
+ natural: water
+ output:
+ - layer: water
+ geometry: polygon
+ tags:
+ natural: water
+ """;
+ private final SchemaSpecification goodSpec = SchemaSpecification.load(goodSpecString);
+
+ private final Profile waterSchema = new Profile() {
+ @Override
+ public void processFeature(SourceFeature sourceFeature, FeatureCollector features) {
+ if (sourceFeature.canBePolygon() && sourceFeature.hasTag("natural", "water")) {
+ features.polygon("water")
+ .setMinPixelSize(10)
+ .inheritAttrFromSource("natural");
+ }
+ }
+
+ @Override
+ public String name() {
+ return "test profile";
+ }
+ };
+
+ private Result validate(Profile profile, String spec) {
+ var result = BaseSchemaValidator.validate(
+ profile,
+ SchemaSpecification.load(spec),
+ PlanetilerConfig.defaults()
+ );
+ for (var example : result.results()) {
+ if (example.issues().isFailure()) {
+ assertNotNull(example.issues().get());
+ }
+ }
+ // also exercise the cli writer and return what it would have printed to stdout
+ var cliOutput = validateCli(profile, SchemaSpecification.load(spec));
+ return new Result(result, cliOutput);
+ }
+
+ private String validateCli(Profile profile, SchemaSpecification spec) {
+ try (
+ var baos = new ByteArrayOutputStream();
+ var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8)
+ ) {
+ new BaseSchemaValidator(Arguments.of(), printStream) {
+ @Override
+ protected Result validate(Set pathsToWatch) {
+ return validate(profile, spec, PlanetilerConfig.from(args));
+ }
+ }.validateFromCli();
+ return baos.toString(StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException {
+ return validate(
+ waterSchema,
+ """
+ examples:
+ - name: test output
+ input:
+ source: osm
+ geometry: polygon
+ tags:
+ natural: water
+ output:
+ layer: %s
+ geometry: %s
+ %s
+ tags:
+ %s
+ """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
+ tags == null ? "" : tags.indent(6).strip())
+ );
+ }
+
+ @ParameterizedTest
+ @CsvSource(value = {
+ "true,water,polygon,natural: water,",
+ "true,water,polygon,,",
+ "true,water,polygon,'natural: water\nother: null',",
+ "false,water,polygon,natural: null,",
+ "false,water2,polygon,natural: water,",
+ "false,water,line,natural: water,",
+ "false,water,line,natural: water,",
+ "false,water,polygon,natural: water2,",
+ "false,water,polygon,'natural: water\nother: value',",
+
+ "true,water,polygon,natural: water,allow_extra_tags: true",
+ "true,water,polygon,natural: water,allow_extra_tags: false",
+ "true,water,polygon,,allow_extra_tags: true",
+ "false,water,polygon,,allow_extra_tags: false",
+
+ "true,water,polygon,,min_size: 10",
+ "false,water,polygon,,min_size: 9",
+ })
+ void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags)
+ throws IOException {
+ var results = validateWater(layer, geometry, tags, allowExtraTags);
+ assertEquals(1, results.output.results().size());
+ assertEquals("test output", results.output.results().get(0).example().name());
+ if (shouldBeOk) {
+ assertTrue(results.output.ok(), results.toString());
+ assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput);
+ } else {
+ assertFalse(results.output.ok(), "Expected an issue, but there were none");
+ assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput);
+ }
+ }
+
+ @Test
+ void testValidationFailsWrongNumberOfFeatures() throws IOException {
+ var results = validate(
+ waterSchema,
+ """
+ examples:
+ - name: test output
+ input:
+ source: osm
+ geometry: polygon
+ tags:
+ natural: water
+ output:
+ """
+ );
+ assertFalse(results.output.ok(), results.toString());
+
+ results = validate(
+ waterSchema,
+ """
+ examples:
+ - name: test output
+ input:
+ source: osm
+ geometry: polygon
+ tags:
+ natural: water
+ output:
+ - layer: water
+ geometry: polygon
+ tags:
+ natural: water
+ - layer: water2
+ geometry: polygon
+ tags:
+ natural: water2
+ """
+ );
+ assertFalse(results.output.ok(), results.toString());
+ }
+
+ @TestFactory
+ Stream testJunitAdapterSpec() {
+ return TestUtils.validateProfile(waterSchema, goodSpec);
+ }
+
+ @TestFactory
+ Stream testJunitAdapterString() {
+ return TestUtils.validateProfile(waterSchema, goodSpecString);
+ }
+
+
+ record Result(BaseSchemaValidator.Result output, String cliOutput) {}
+}
diff --git a/planetiler-custommap/pom.xml b/planetiler-custommap/pom.xml
index 40548cc7e2..c02e323e21 100644
--- a/planetiler-custommap/pom.xml
+++ b/planetiler-custommap/pom.xml
@@ -18,10 +18,6 @@
planetiler-core
${project.parent.version}
-
- org.snakeyaml
- snakeyaml-engine
-
org.commonmark
commonmark
diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java
index 23283e531d..ff73a1f56c 100644
--- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java
+++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/ConfiguredMapMain.java
@@ -5,6 +5,7 @@
import com.onthegomap.planetiler.custommap.configschema.DataSourceType;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
import com.onthegomap.planetiler.custommap.expression.ParseException;
+import com.onthegomap.planetiler.util.YAML;
import java.nio.file.Files;
import java.nio.file.Path;
diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java
index c4b70912a9..fc11d488cc 100644
--- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java
+++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/SchemaConfig.java
@@ -1,7 +1,7 @@
package com.onthegomap.planetiler.custommap.configschema;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.onthegomap.planetiler.custommap.YAML;
+import com.onthegomap.planetiler.util.YAML;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java
index b112a189fb..53b9f64d1d 100644
--- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java
+++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/validator/SchemaValidator.java
@@ -1,44 +1,31 @@
package com.onthegomap.planetiler.custommap.validator;
import com.fasterxml.jackson.core.JacksonException;
-import com.onthegomap.planetiler.FeatureCollector;
-import com.onthegomap.planetiler.Profile;
import com.onthegomap.planetiler.config.Arguments;
-import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.custommap.ConfiguredProfile;
import com.onthegomap.planetiler.custommap.Contexts;
-import com.onthegomap.planetiler.custommap.YAML;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
-import com.onthegomap.planetiler.geo.GeometryType;
-import com.onthegomap.planetiler.reader.SimpleFeature;
-import com.onthegomap.planetiler.stats.Stats;
import com.onthegomap.planetiler.util.AnsiColors;
-import com.onthegomap.planetiler.util.FileWatcher;
-import com.onthegomap.planetiler.util.Format;
-import com.onthegomap.planetiler.util.Try;
+import com.onthegomap.planetiler.util.YAML;
+import com.onthegomap.planetiler.validator.BaseSchemaValidator;
+import com.onthegomap.planetiler.validator.SchemaSpecification;
import java.io.PrintStream;
import java.nio.file.Path;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
import java.util.Set;
-import java.util.TreeSet;
import java.util.stream.Stream;
import org.apache.commons.lang3.exception.ExceptionUtils;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.io.ParseException;
-import org.locationtech.jts.io.WKTReader;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
-/** Verifies that a profile maps input elements map to expected output vector tile features. */
-public class SchemaValidator {
+public class SchemaValidator extends BaseSchemaValidator {
- private static final String PASS_BADGE = AnsiColors.greenBackground(" PASS ");
- private static final String FAIL_BADGE = AnsiColors.redBackground(" FAIL ");
+ private final Path schemaPath;
+
+ SchemaValidator(Arguments args, String schemaFile, PrintStream output) {
+ super(args, output);
+ schemaPath = schemaFile == null ? args.inputFile("schema", "Schema file") :
+ args.inputFile("schema", "Schema file", Path.of(schemaFile));
+ }
public static void main(String[] args) {
// let users run `verify schema.yml` as a shortcut
@@ -48,36 +35,24 @@ public static void main(String[] args) {
args = Stream.of(args).skip(1).toArray(String[]::new);
}
var arguments = Arguments.fromEnvOrArgs(args);
- var schema = schemaFile == null ? arguments.inputFile("schema", "Schema file") :
- arguments.inputFile("schema", "Schema file", Path.of(schemaFile));
- var watch =
- arguments.getBoolean("watch", "Watch files for changes and re-run validation when schema or spec changes", false);
-
-
- PrintStream output = System.out;
- output.println("OK");
- var paths = validateFromCli(schema, output);
-
- if (watch) {
- output.println();
- output.println("Watching filesystem for changes...");
- var watcher = FileWatcher.newWatcher(paths.toArray(Path[]::new));
- watcher.pollForChanges(Duration.ofMillis(300), changed -> validateFromCli(schema, output));
- }
+ new SchemaValidator(arguments, schemaFile, System.out).runOrWatch();
}
- private static boolean hasCause(Throwable t, Class> cause) {
- return t != null && (cause.isInstance(t) || hasCause(t.getCause(), cause));
+
+ /**
+ * Returns the result of validating the profile defined by {@code schema} against the examples in
+ * {@code specification}.
+ */
+ public static Result validate(SchemaConfig schema, SchemaSpecification specification) {
+ var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
+ return validate(new ConfiguredProfile(schema, context), specification, context.config());
}
- static Set validateFromCli(Path schemaPath, PrintStream output) {
- Set pathsToWatch = new HashSet<>();
- pathsToWatch.add(schemaPath);
- output.println();
- output.println("Validating...");
- output.println();
- SchemaValidator.Result result;
+ @Override
+ protected Result validate(Set pathsToWatch) {
+ Result result = null;
try {
+ pathsToWatch.add(schemaPath);
var schema = SchemaConfig.load(schemaPath);
var examples = schema.examples();
// examples can either be embedded in the yaml file, or referenced
@@ -108,169 +83,7 @@ static Set validateFromCli(Path schemaPath, PrintStream output) {
String.join("\n", ExceptionUtils.getStackTrace(rootCause)))
.indent(4));
}
- return pathsToWatch;
- }
- int failed = 0, passed = 0;
- List failures = new ArrayList<>();
- for (var example : result.results) {
- if (example.ok()) {
- passed++;
- output.printf("%s %s%n", PASS_BADGE, example.example().name());
- } else {
- failed++;
- printFailure(example, output);
- failures.add(example);
- }
- }
- if (!failures.isEmpty()) {
- output.println();
- output.println("Summary of failures:");
- for (var failure : failures) {
- printFailure(failure, output);
- }
- }
- List summary = new ArrayList<>();
- boolean none = (passed + failed) == 0;
- if (none || failed > 0) {
- summary.add(AnsiColors.redBold(failed + " failed"));
- }
- if (none || passed > 0) {
- summary.add(AnsiColors.greenBold(passed + " passed"));
- }
- if (none || passed > 0 && failed > 0) {
- summary.add((failed + passed) + " total");
- }
- output.println();
- output.println(String.join(", ", summary));
- return pathsToWatch;
- }
-
- private static void printFailure(ExampleResult example, PrintStream output) {
- output.printf("%s %s%n", FAIL_BADGE, example.example().name());
- if (example.issues.isFailure()) {
- output.println(ExceptionUtils.getStackTrace(example.issues.exception()).indent(4).stripTrailing());
- } else {
- for (var issue : example.issues().get()) {
- output.println(" ● " + issue.indent(4).strip());
- }
- }
- }
-
- private static Geometry parseGeometry(String geometry) {
- String wkt = switch (geometry.toLowerCase(Locale.ROOT).trim()) {
- case "point" -> "POINT (0 0)";
- case "line" -> "LINESTRING (0 0, 1 1)";
- case "polygon" -> "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))";
- default -> geometry;
- };
- try {
- return new WKTReader().read(wkt);
- } catch (ParseException e) {
- throw new IllegalArgumentException("""
- Bad geometry: "%s", must be "point" "line" "polygon" or a valid WKT string.
- """.formatted(geometry));
- }
- }
-
- /**
- * Returns the result of validating the profile defined by {@code schema} against the examples in
- * {@code specification}.
- */
- public static Result validate(SchemaConfig schema, SchemaSpecification specification) {
- var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
- return validate(new ConfiguredProfile(schema, context), specification, context.config());
- }
-
- /** Returns the result of validating {@code profile} against the examples in {@code specification}. */
- public static Result validate(Profile profile, SchemaSpecification specification, PlanetilerConfig config) {
- var featureCollectorFactory = new FeatureCollector.Factory(config, Stats.inMemory());
- return new Result(specification.examples().stream().map(example -> new ExampleResult(example, Try.apply(() -> {
- List issues = new ArrayList<>();
- var input = example.input();
- var expectedFeatures = example.output();
- var geometry = parseGeometry(input.geometry());
- var feature = SimpleFeature.create(geometry, input.tags(), input.source(), null, 0);
- var collector = featureCollectorFactory.get(feature);
- profile.processFeature(feature, collector);
- List result = new ArrayList<>();
- collector.forEach(result::add);
- if (result.size() != expectedFeatures.size()) {
- issues.add(
- "Different number of elements, expected=%s actual=%s".formatted(expectedFeatures.size(), result.size()));
- } else {
- // TODO print a diff of the input and output feature YAML representations
- for (int i = 0; i < expectedFeatures.size(); i++) {
- var expected = expectedFeatures.get(i);
- var actual = result.stream().max(proximityTo(expected)).orElseThrow();
- result.remove(actual);
- var actualTags = actual.getAttrsAtZoom(expected.atZoom());
- String prefix = "feature[%d]".formatted(i);
- validate(prefix + ".layer", issues, expected.layer(), actual.getLayer());
- validate(prefix + ".minzoom", issues, expected.minZoom(), actual.getMinZoom());
- validate(prefix + ".maxzoom", issues, expected.maxZoom(), actual.getMaxZoom());
- validate(prefix + ".minsize", issues, expected.minSize(), actual.getMinPixelSizeAtZoom(expected.atZoom()));
- validate(prefix + ".geometry", issues, expected.geometry(), GeometryType.typeOf(actual.getGeometry()));
- Set tags = new TreeSet<>(actualTags.keySet());
- expected.tags().forEach((tag, value) -> {
- validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, value, actualTags.get(tag), false);
- tags.remove(tag);
- });
- if (Boolean.FALSE.equals(expected.allowExtraTags())) {
- for (var tag : tags) {
- validate(prefix + ".tags[\"%s\"]".formatted(tag), issues, null, actualTags.get(tag), false);
- }
- }
- }
- }
- return issues;
- }))).toList());
- }
-
- private static Comparator proximityTo(SchemaSpecification.OutputFeature expected) {
- return Comparator.comparingInt(item -> (Objects.equals(item.getLayer(), expected.layer()) ? 2 : 0) +
- (Objects.equals(GeometryType.typeOf(item.getGeometry()), expected.geometry()) ? 1 : 0));
- }
-
- private static void validate(String field, List issues, T expected, T actual, boolean ignoreWhenNull) {
- if ((!ignoreWhenNull || expected != null) && !Objects.equals(expected, actual)) {
- // handle when expected and actual are int/long or long/int
- if (expected instanceof Number && actual instanceof Number && expected.toString().equals(actual.toString())) {
- return;
- }
- issues.add("%s: expected <%s> actual <%s>".formatted(field, format(expected), format(actual)));
- }
- }
-
- private static String format(Object o) {
- if (o == null) {
- return "null";
- } else if (o instanceof String s) {
- return Format.quote(s);
- } else {
- return o.toString();
- }
- }
-
- private static void validate(String field, List issues, T expected, T actual) {
- validate(field, issues, expected, actual, true);
- }
-
- /** Result of comparing the output vector tile feature to what was expected. */
- public record ExampleResult(
- SchemaSpecification.Example example,
- // TODO include a symmetric diff so we can pretty-print the expected/actual output diff
- Try> issues
- ) {
-
- public boolean ok() {
- return issues.isSuccess() && issues.get().isEmpty();
- }
- }
-
- public record Result(List results) {
-
- public boolean ok() {
- return results.stream().allMatch(ExampleResult::ok);
}
+ return result;
}
}
diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java
index b7dec4a8d8..5b17b81b59 100644
--- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java
+++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/BooleanExpressionParserTest.java
@@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.onthegomap.planetiler.expression.Expression;
+import com.onthegomap.planetiler.util.YAML;
import java.util.Map;
import org.junit.jupiter.api.Test;
diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java
index 78f8804e25..46c25652ea 100644
--- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java
+++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/ConfigExpressionParserTest.java
@@ -8,6 +8,7 @@
import com.onthegomap.planetiler.expression.DataType;
import com.onthegomap.planetiler.expression.Expression;
import com.onthegomap.planetiler.expression.MultiExpression;
+import com.onthegomap.planetiler.util.YAML;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java
index 3400fe668d..ab9c918366 100644
--- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java
+++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/SchemaTests.java
@@ -1,35 +1,26 @@
package com.onthegomap.planetiler.custommap;
-import static org.junit.jupiter.api.DynamicTest.dynamicTest;
-
+import com.onthegomap.planetiler.TestUtils;
+import com.onthegomap.planetiler.config.Arguments;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
-import com.onthegomap.planetiler.custommap.validator.SchemaSpecification;
-import com.onthegomap.planetiler.custommap.validator.SchemaValidator;
+import com.onthegomap.planetiler.validator.SchemaSpecification;
import java.nio.file.Path;
-import java.util.List;
-import org.junit.jupiter.api.DynamicTest;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.TestFactory;
class SchemaTests {
@TestFactory
- List shortbread() {
- return testSchema("shortbread.yml", "shortbread.spec.yml");
+ Stream shortbread() {
+ return test("shortbread.yml", "shortbread.spec.yml");
}
- private List testSchema(String schema, String spec) {
+ private static Stream test(String schemaFile, String specFile) {
var base = Path.of("src", "main", "resources", "samples");
- var result = SchemaValidator.validate(
- SchemaConfig.load(base.resolve(schema)),
- SchemaSpecification.load(base.resolve(spec))
- );
- return result.results().stream()
- .map(test -> dynamicTest(test.example().name(), () -> {
- if (test.issues().isFailure()) {
- throw test.issues().exception();
- }
- if (!test.issues().get().isEmpty()) {
- throw new AssertionError("Validation failed:\n" + String.join("\n", test.issues().get()));
- }
- })).toList();
+ SchemaConfig schema = SchemaConfig.load(base.resolve(schemaFile));
+ SchemaSpecification specification = SchemaSpecification.load(base.resolve(specFile));
+ var context = Contexts.buildRootContext(Arguments.of().silence(), schema.args());
+ var profile = new ConfiguredProfile(schema, context);
+ return TestUtils.validateProfile(profile, specification);
}
}
diff --git a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java
index eff36cc9c9..3672493398 100644
--- a/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java
+++ b/planetiler-custommap/src/test/java/com/onthegomap/planetiler/custommap/validator/SchemaValidatorTest.java
@@ -5,7 +5,10 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.onthegomap.planetiler.config.Arguments;
import com.onthegomap.planetiler.custommap.configschema.SchemaConfig;
+import com.onthegomap.planetiler.validator.BaseSchemaValidator;
+import com.onthegomap.planetiler.validator.SchemaSpecification;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
@@ -22,7 +25,7 @@ class SchemaValidatorTest {
@TempDir
Path tmpDir;
- record Result(SchemaValidator.Result output, String cliOutput) {}
+ record Result(BaseSchemaValidator.Result output, String cliOutput) {}
private Result validate(String schema, String spec) throws IOException {
var result = SchemaValidator.validate(
@@ -57,55 +60,13 @@ private String validateCli(Path path) {
var baos = new ByteArrayOutputStream();
var printStream = new PrintStream(baos, true, StandardCharsets.UTF_8)
) {
- SchemaValidator.validateFromCli(
- path,
- printStream
- );
+ new SchemaValidator(Arguments.of(), path.toString(), printStream).validateFromCli();
return baos.toString(StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
- String waterSchema = """
- sources:
- osm:
- type: osm
- url: geofabrik:rhode-island
- layers:
- - id: water
- features:
- - source: osm
- geometry: polygon
- min_size: 10
- include_when:
- natural: water
- attributes:
- - key: natural
- """;
-
- private Result validateWater(String layer, String geometry, String tags, String allowExtraTags) throws IOException {
- return validate(
- waterSchema,
- """
- examples:
- - name: test output
- input:
- source: osm
- geometry: polygon
- tags:
- natural: water
- output:
- layer: %s
- geometry: %s
- %s
- tags:
- %s
- """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
- tags == null ? "" : tags.indent(6).strip())
- );
- }
-
@ParameterizedTest
@CsvSource(value = {
"true,water,polygon,natural: water,",
@@ -128,37 +89,23 @@ private Result validateWater(String layer, String geometry, String tags, String
})
void testValidateWaterPolygon(boolean shouldBeOk, String layer, String geometry, String tags, String allowExtraTags)
throws IOException {
- var results = validateWater(layer, geometry, tags, allowExtraTags);
- assertEquals(1, results.output.results().size());
- assertEquals("test output", results.output.results().get(0).example().name());
- if (shouldBeOk) {
- assertTrue(results.output.ok(), results.toString());
- assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput);
- } else {
- assertFalse(results.output.ok(), "Expected an issue, but there were none");
- assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput);
- }
- }
-
- @Test
- void testValidationFailsWrongNumberOfFeatures() throws IOException {
var results = validate(
- waterSchema,
"""
- examples:
- - name: test output
- input:
- source: osm
+ sources:
+ osm:
+ type: osm
+ url: geofabrik:rhode-island
+ layers:
+ - id: water
+ features:
+ - source: osm
geometry: polygon
- tags:
+ min_size: 10
+ include_when:
natural: water
- output:
- """
- );
- assertFalse(results.output.ok(), results.toString());
-
- results = validate(
- waterSchema,
+ attributes:
+ - key: natural
+ """,
"""
examples:
- name: test output
@@ -168,17 +115,23 @@ void testValidationFailsWrongNumberOfFeatures() throws IOException {
tags:
natural: water
output:
- - layer: water
- geometry: polygon
- tags:
- natural: water
- - layer: water2
- geometry: polygon
+ layer: %s
+ geometry: %s
+ %s
tags:
- natural: water2
- """
+ %s
+ """.formatted(layer, geometry, allowExtraTags == null ? "" : allowExtraTags,
+ tags == null ? "" : tags.indent(6).strip())
);
- assertFalse(results.output.ok(), results.toString());
+ assertEquals(1, results.output.results().size());
+ assertEquals("test output", results.output.results().get(0).example().name());
+ if (shouldBeOk) {
+ assertTrue(results.output.ok(), results.toString());
+ assertFalse(results.cliOutput.contains("FAIL"), "contained FAIL but should not have: " + results.cliOutput);
+ } else {
+ assertFalse(results.output.ok(), "Expected an issue, but there were none");
+ assertTrue(results.cliOutput.contains("FAIL"), "did not contain FAIL but should have: " + results.cliOutput);
+ }
}
@Test
diff --git a/planetiler-dist/pom.xml b/planetiler-dist/pom.xml
index 7af1b23c7a..86f3092ce1 100644
--- a/planetiler-dist/pom.xml
+++ b/planetiler-dist/pom.xml
@@ -43,6 +43,11 @@
planetiler-custommap
${project.parent.version}
+
+ com.onthegomap.planetiler
+ planetiler-experimental
+ ${project.parent.version}
+
com.onthegomap.planetiler
planetiler-examples
diff --git a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
index df949ed305..3ce174b325 100644
--- a/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
+++ b/planetiler-dist/src/main/java/com/onthegomap/planetiler/Main.java
@@ -10,6 +10,9 @@
import com.onthegomap.planetiler.examples.OsmQaTiles;
import com.onthegomap.planetiler.examples.ToiletsOverlay;
import com.onthegomap.planetiler.examples.ToiletsOverlayLowLevelApi;
+import com.onthegomap.planetiler.experimental.lua.GenerateLuaTypes;
+import com.onthegomap.planetiler.experimental.lua.LuaMain;
+import com.onthegomap.planetiler.experimental.lua.LuaValidator;
import com.onthegomap.planetiler.mbtiles.Verify;
import com.onthegomap.planetiler.util.TileSizeStats;
import com.onthegomap.planetiler.util.TopOsmTiles;
@@ -43,12 +46,20 @@ public class Main {
entry("generate-custom", ConfiguredMapMain::main),
entry("custom", ConfiguredMapMain::main),
+ entry("lua", LuaMain::main),
+ entry("lua-types", GenerateLuaTypes::main),
+
entry("generate-shortbread", bundledSchema("shortbread.yml")),
entry("shortbread", bundledSchema("shortbread.yml")),
- entry("verify", SchemaValidator::main),
+ entry("verify", validate()),
entry("verify-custom", SchemaValidator::main),
entry("verify-schema", SchemaValidator::main),
+ entry("verify-lua", LuaValidator::main),
+ entry("validate", validate()),
+ entry("validate-custom", SchemaValidator::main),
+ entry("validate-schema", SchemaValidator::main),
+ entry("validate-lua", LuaValidator::main),
entry("example-bikeroutes", BikeRouteOverlay::main),
entry("example-toilets", ToiletsOverlay::main),
@@ -73,6 +84,16 @@ private static EntryPoint bundledSchema(String path) {
).toArray(String[]::new));
}
+ private static EntryPoint validate() {
+ return args -> {
+ if (Arrays.stream(args).anyMatch(d -> d.endsWith(".lua"))) {
+ LuaValidator.main(args);
+ } else {
+ SchemaValidator.main(args);
+ }
+ };
+ }
+
public static void main(String[] args) throws Exception {
EntryPoint task = DEFAULT_TASK;
@@ -81,6 +102,9 @@ public static void main(String[] args) throws Exception {
if (maybeTask.matches("^.*\\.ya?ml$")) {
task = ConfiguredMapMain::main;
args[0] = "--schema=" + args[0];
+ } else if (maybeTask.matches("^.*\\.lua$")) {
+ task = LuaMain::main;
+ args[0] = "--script=" + args[0];
} else {
EntryPoint taskFromArg0 = ENTRY_POINTS.get(maybeTask);
if (taskFromArg0 != null) {
diff --git a/planetiler-experimental/pom.xml b/planetiler-experimental/pom.xml
new file mode 100644
index 0000000000..b11a29e351
--- /dev/null
+++ b/planetiler-experimental/pom.xml
@@ -0,0 +1,43 @@
+
+
+
+ 4.0.0
+
+ planetiler-experimental
+
+
+ com.onthegomap.planetiler
+ planetiler-parent
+ ${revision}
+
+
+
+
+ com.onthegomap.planetiler
+ planetiler-core
+ ${project.parent.version}
+
+
+
+ org.luaj
+ luaj-jse
+ 3.0.1
+
+
+ org.apache.bcel
+ bcel
+ 6.7.0
+
+
+
+
+ com.onthegomap.planetiler
+ planetiler-core
+ ${project.parent.version}
+ test-jar
+ test
+
+
+
diff --git a/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java
new file mode 100644
index 0000000000..b2444b27a7
--- /dev/null
+++ b/planetiler-experimental/src/main/java/com/onthegomap/planetiler/experimental/lua/GenerateLuaTypes.java
@@ -0,0 +1,499 @@
+package com.onthegomap.planetiler.experimental.lua;
+
+import static com.onthegomap.planetiler.experimental.lua.JavaToLuaCase.transformMemberName;
+import static java.util.Map.entry;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.reflect.Invokable;
+import com.google.common.reflect.TypeToken;
+import com.onthegomap.planetiler.util.Format;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.RecordComponent;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import org.luaj.vm2.LuaDouble;
+import org.luaj.vm2.LuaInteger;
+import org.luaj.vm2.LuaNumber;
+import org.luaj.vm2.LuaString;
+import org.luaj.vm2.LuaTable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.lib.jse.LuaBindMethods;
+import org.luaj.vm2.lib.jse.LuaFunctionType;
+import org.luaj.vm2.lib.jse.LuaGetter;
+import org.luaj.vm2.lib.jse.LuaSetter;
+import org.luaj.vm2.lib.jse.LuaType;
+
+/**
+ * Generates a lua file with type definitions for the lua environment exposed by planetiler.
+ *
+ *
+ * java -jar planetiler.jar lua-types > types.lua
+ *
+ *
+ * @see Lua Language Server type annotations
+ */
+public class GenerateLuaTypes {
+ private static final Map, String> TYPE_NAMES = Map.ofEntries(
+ entry(Object.class, "any"),
+ entry(LuaInteger.class, "integer"),
+ entry(LuaDouble.class, "number"),
+ entry(LuaNumber.class, "number"),
+ entry(LuaString.class, "string"),
+ entry(LuaTable.class, "table"),
+ entry(Class.class, "userdata"),
+ entry(String.class, "string"),
+ entry(Number.class, "number"),
+ entry(byte[].class, "string"),
+ entry(Integer.class, "integer"),
+ entry(int.class, "integer"),
+ entry(Long.class, "integer"),
+ entry(long.class, "integer"),
+ entry(Short.class, "integer"),
+ entry(short.class, "integer"),
+ entry(Byte.class, "integer"),
+ entry(byte.class, "integer"),
+ entry(Double.class, "number"),
+ entry(double.class, "number"),
+ entry(Float.class, "number"),
+ entry(float.class, "number"),
+ entry(boolean.class, "boolean"),
+ entry(Boolean.class, "boolean"),
+ entry(Void.class, "nil"),
+ entry(void.class, "nil")
+ );
+ private static final TypeToken> LIST_TYPE = new TypeToken<>() {};
+ private static final TypeToken