From b97d9d54094fd1a3bb28be74a86b4510518742c8 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:50:56 +0700 Subject: [PATCH] Write the public surface down, and check it every build api/surface.txt has one line for every exported type and every member a caller outside the module can name, and SurfaceTest regenerates it and compares. A change to the API is now a change to that file, in the same commit, where it is the first thing in a diff rather than something a user finds after the release. This is the same mechanism zu-go uses, held to the same file name and the same place, because a reviewer moving between the clients should not have to learn a second one. Write the file with mvn -pl zudb test -Dzu.surface.write=true, which is the one command to reach for when the change was meant, and running it is how you say so. Nothing here comes from Class.toGenericString. That method is not the same on every JDK the api job builds for: 21 leaves the sealed marker out and 25 puts it in, so the first version of this reported a change nobody made as soon as it ran on the matrix. Everything is spelled out off the class file instead, and the output is byte for byte identical on 21 and 25. The failure prints the lines that moved rather than the file twice. Five hundred lines against five hundred lines is a report nobody reads, and the two lines that changed are the report. --- README.md | 16 + api/surface.txt | 512 +++++++++++++++++++ zudb/src/test/java/dev/zudb/SurfaceTest.java | 324 ++++++++++++ 3 files changed, 852 insertions(+) create mode 100644 api/surface.txt create mode 100644 zudb/src/test/java/dev/zudb/SurfaceTest.java diff --git a/README.md b/README.md index f103d60..f6d72c4 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,22 @@ Pre-1.0 and pre-release. Nothing is published yet. The engine, the C ABI, and th If a bug reproduces through the `zu` CLI, it belongs in [tamnd/zu](https://github.com/tamnd/zu/issues), not here. +Inside this repository: + +| What | Where | +|---|---| +| The API, and nothing that touches the library | `zudb` | +| The Panama provider | `zudb-ffm` | +| The JNI provider, and the C shim it calls through | `zudb-jni` | +| The cases every provider owes, run by both of them | `zudb-tck` | +| Arrow, over the C Data Interface | `zudb-arrow` | +| JMH benchmarks | `zudb-bench` | +| The staged libraries, built by the release rather than by a clone | `zudb-native` | +| Every published name and the shape it is published in | `api/surface.txt` | +| Building the shim, staging the libraries | `scripts` | + +`api/surface.txt` is generated, one line per exported type and per member a caller outside the module can name, in the spirit of the `api/go1.N.txt` files Go holds itself to. `SurfaceTest` regenerates it and compares, so a change to the API is a change to that file in the same commit, where it is the first thing in the diff rather than something a user finds after the release. Write it down with `mvn -pl zudb test -Dzu.surface.write=true` and review it like any other file: a name that arrived is a minor release, a name that went or changed shape is a major one or a mistake, and the gate says which of the three a diff is while it is still a diff. It reads the compiled classes of the API module and links nothing, so it answers on a clone with no library staged and no Rust installed. + ## License Apache-2.0, same as the engine. diff --git a/api/surface.txt b/api/surface.txt new file mode 100644 index 0000000..5033c13 --- /dev/null +++ b/api/surface.txt @@ -0,0 +1,512 @@ +ctor public dev.zudb.Config(long, long, boolean) +ctor public dev.zudb.Diagnostic(dev.zudb.Status, java.lang.String, java.lang.String, java.lang.String, dev.zudb.Severity, int, int, int, java.lang.String, java.lang.String, boolean) +ctor public dev.zudb.Value$BindingTable() +ctor public dev.zudb.Value$Bool(boolean) +ctor public dev.zudb.Value$Field(java.lang.String, dev.zudb.Value) +ctor public dev.zudb.Value$Float(double) +ctor public dev.zudb.Value$Graph() +ctor public dev.zudb.Value$Int(long) +ctor public dev.zudb.Value$List(java.util.List) +ctor public dev.zudb.Value$Node(int, long) +ctor public dev.zudb.Value$Null() +ctor public dev.zudb.Value$Path(java.util.List) +ctor public dev.zudb.Value$Record(java.util.List) +ctor public dev.zudb.Value$Rel(int, long, long) +ctor public dev.zudb.Value$Str(java.lang.String) +ctor public dev.zudb.Value$Temporal(dev.zudb.Value$Temporal$Kind, long, int) +ctor public dev.zudb.ZuException$Position(int, int, int) +ctor public dev.zudb.spi.ProviderUnavailableException(java.lang.String) +ctor public dev.zudb.spi.ProviderUnavailableException(java.lang.String, java.lang.Throwable) +field public static final dev.zudb.Severity dev.zudb.Severity.EXCEPTION +field public static final dev.zudb.Severity dev.zudb.Severity.INFORMATIONAL +field public static final dev.zudb.Severity dev.zudb.Severity.NO_DATA +field public static final dev.zudb.Severity dev.zudb.Severity.SUCCESS +field public static final dev.zudb.Severity dev.zudb.Severity.WARNING +field public static final dev.zudb.Status dev.zudb.Status.CONFLICT +field public static final dev.zudb.Status dev.zudb.Status.CORRUPT +field public static final dev.zudb.Status dev.zudb.Status.DONE +field public static final dev.zudb.Status dev.zudb.Status.ERROR +field public static final dev.zudb.Status dev.zudb.Status.INTERRUPTED +field public static final dev.zudb.Status dev.zudb.Status.IO +field public static final dev.zudb.Status dev.zudb.Status.MISUSE +field public static final dev.zudb.Status dev.zudb.Status.MISUSE_CLOSED +field public static final dev.zudb.Status dev.zudb.Status.MISUSE_CONCURRENT +field public static final dev.zudb.Status dev.zudb.Status.OK +field public static final dev.zudb.Status dev.zudb.Status.UNKNOWN +field public static final dev.zudb.Status dev.zudb.Status.UNSUPPORTED +field public static final dev.zudb.Type dev.zudb.Type.BINDING_TABLE +field public static final dev.zudb.Type dev.zudb.Type.BOOL +field public static final dev.zudb.Type dev.zudb.Type.FLOAT +field public static final dev.zudb.Type dev.zudb.Type.GRAPH +field public static final dev.zudb.Type dev.zudb.Type.INT +field public static final dev.zudb.Type dev.zudb.Type.LIST +field public static final dev.zudb.Type dev.zudb.Type.NODE +field public static final dev.zudb.Type dev.zudb.Type.NULL +field public static final dev.zudb.Type dev.zudb.Type.PATH +field public static final dev.zudb.Type dev.zudb.Type.RECORD +field public static final dev.zudb.Type dev.zudb.Type.REL +field public static final dev.zudb.Type dev.zudb.Type.STR +field public static final dev.zudb.Type dev.zudb.Type.TEMPORAL +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.DATE +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.DURATION_DAY_TIME +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.DURATION_YEAR_MONTH +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.LOCAL_DATETIME +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.LOCAL_TIME +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.ZONED_DATETIME +field public static final dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.ZONED_TIME +field public static final java.lang.String dev.zudb.Zu.ABI_VERSION +field public static final long dev.zudb.Result.DEFAULT_BATCH +method public T dev.zudb.Connection.transaction(java.util.function.Supplier) +method public abstract boolean dev.zudb.Progress.at(long, long) +method public abstract boolean dev.zudb.spi.ZuBinding.connInTransaction(long) +method public abstract boolean dev.zudb.spi.ZuBinding.connUnregister(long, java.lang.String) +method public abstract boolean dev.zudb.spi.ZuBinding.databaseIsMemory(long) +method public abstract boolean dev.zudb.spi.ZuBinding.valueBoolean(long) +method public abstract dev.zudb.Diagnostic dev.zudb.spi.ZuBinding.resultNotice(long, int) +method public abstract dev.zudb.spi.ZuBinding dev.zudb.spi.ZuProvider.load(java.nio.file.Path) +method public abstract double dev.zudb.spi.ZuBinding.valueDouble(long) +method public abstract int dev.zudb.spi.ZuBinding.appenderColumns(long) +method public abstract int dev.zudb.spi.ZuBinding.resultCellType(long, long, int) +method public abstract int dev.zudb.spi.ZuBinding.resultCols(long) +method public abstract int dev.zudb.spi.ZuBinding.resultNotices(long) +method public abstract int dev.zudb.spi.ZuBinding.valueType(long) +method public abstract int dev.zudb.spi.ZuProvider.priority() +method public abstract java.lang.String dev.zudb.spi.ZuBinding.appenderColumnName(long, int) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.connRegisteredName(long, long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.databasePath(long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultCellString(long, long, int) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultColName(long, int) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultGqlstatus(long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.valueField(long, long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.valueString(long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.version() +method public abstract java.lang.String dev.zudb.spi.ZuProvider.name() +method public abstract java.nio.ByteBuffer dev.zudb.spi.ZuBinding.chunkValid(long, long, int, long) +method public abstract java.nio.ByteBuffer dev.zudb.spi.ZuBinding.colValid(long, int, long) +method public abstract java.nio.DoubleBuffer dev.zudb.spi.ZuBinding.chunkDoubles(long, long, int, long) +method public abstract java.nio.DoubleBuffer dev.zudb.spi.ZuBinding.colDoubles(long, int, long) +method public abstract java.nio.LongBuffer dev.zudb.spi.ZuBinding.chunkLongs(long, long, int, long) +method public abstract java.nio.LongBuffer dev.zudb.spi.ZuBinding.chunkNodeOffsets(long, long, int, long) +method public abstract java.nio.LongBuffer dev.zudb.spi.ZuBinding.colLongs(long, int, long) +method public abstract java.nio.LongBuffer dev.zudb.spi.ZuBinding.colNodeOffsets(long, int, long) +method public abstract long dev.zudb.spi.ZuBinding.appenderBuffered(long) +method public abstract long dev.zudb.spi.ZuBinding.appenderClose(long) +method public abstract long dev.zudb.spi.ZuBinding.appenderCommitted(long) +method public abstract long dev.zudb.spi.ZuBinding.appenderDiscard(long) +method public abstract long dev.zudb.spi.ZuBinding.appenderOpen(long, java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.chunkCount(long) +method public abstract long dev.zudb.spi.ZuBinding.connDuplicate(long) +method public abstract long dev.zudb.spi.ZuBinding.connRegisteredCount(long) +method public abstract long dev.zudb.spi.ZuBinding.connRowsRead(long) +method public abstract long dev.zudb.spi.ZuBinding.connect(long) +method public abstract long dev.zudb.spi.ZuBinding.create(java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.databaseCreate(java.lang.String, long, long, boolean) +method public abstract long dev.zudb.spi.ZuBinding.databaseMemory(long, long, boolean) +method public abstract long dev.zudb.spi.ZuBinding.databaseOpen(java.lang.String, long, long, boolean) +method public abstract long dev.zudb.spi.ZuBinding.execute(long) +method public abstract long dev.zudb.spi.ZuBinding.frameNew(java.lang.String, long, java.lang.Runnable) +method public abstract long dev.zudb.spi.ZuBinding.loaderCreate(java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.memory() +method public abstract long dev.zudb.spi.ZuBinding.open(java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.prepare(long, java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.query(long, java.lang.String) +method public abstract long dev.zudb.spi.ZuBinding.resultCell(long, long, int) +method public abstract long dev.zudb.spi.ZuBinding.resultRows(long) +method public abstract long dev.zudb.spi.ZuBinding.valueAt(long, long) +method public abstract long dev.zudb.spi.ZuBinding.valueLength(long) +method public abstract long dev.zudb.spi.ZuBinding.valueLong(long) +method public abstract long[] dev.zudb.spi.ZuBinding.chunk(long, long) +method public abstract long[] dev.zudb.spi.ZuBinding.configSet(long, long, boolean, java.lang.String, java.lang.String) +method public abstract long[] dev.zudb.spi.ZuBinding.valueNode(long) +method public abstract long[] dev.zudb.spi.ZuBinding.valueRel(long) +method public abstract long[] dev.zudb.spi.ZuBinding.valueTemporal(long) +method public abstract void dev.zudb.spi.ZuBinding.appendBoolean(long, boolean) +method public abstract void dev.zudb.spi.ZuBinding.appendBytes(long, java.nio.ByteBuffer) +method public abstract void dev.zudb.spi.ZuBinding.appendDouble(long, double) +method public abstract void dev.zudb.spi.ZuBinding.appendEndRow(long) +method public abstract void dev.zudb.spi.ZuBinding.appendLong(long, long) +method public abstract void dev.zudb.spi.ZuBinding.appendString(long, java.lang.String) +method public abstract void dev.zudb.spi.ZuBinding.appendTemporal(long, int, long) +method public abstract void dev.zudb.spi.ZuBinding.appenderFlush(long) +method public abstract void dev.zudb.spi.ZuBinding.appenderFree(long) +method public abstract void dev.zudb.spi.ZuBinding.begin(long, boolean) +method public abstract void dev.zudb.spi.ZuBinding.bindBoolean(long, java.lang.String, boolean) +method public abstract void dev.zudb.spi.ZuBinding.bindDouble(long, java.lang.String, double) +method public abstract void dev.zudb.spi.ZuBinding.bindLong(long, java.lang.String, long) +method public abstract void dev.zudb.spi.ZuBinding.bindNull(long, java.lang.String) +method public abstract void dev.zudb.spi.ZuBinding.bindString(long, java.lang.String, java.lang.String) +method public abstract void dev.zudb.spi.ZuBinding.bindTemporal(long, java.lang.String, int, long, int) +method public abstract void dev.zudb.spi.ZuBinding.commit(long) +method public abstract void dev.zudb.spi.ZuBinding.connClose(long) +method public abstract void dev.zudb.spi.ZuBinding.connInterrupt(long) +method public abstract void dev.zudb.spi.ZuBinding.connRegister(long, long) +method public abstract void dev.zudb.spi.ZuBinding.connSetProgress(long, dev.zudb.Progress, long) +method public abstract void dev.zudb.spi.ZuBinding.databaseClose(long) +method public abstract void dev.zudb.spi.ZuBinding.frameColumnBooleans(long, java.lang.String, java.nio.Buffer, long) +method public abstract void dev.zudb.spi.ZuBinding.frameColumnFloats(long, java.lang.String, java.nio.Buffer, long, int) +method public abstract void dev.zudb.spi.ZuBinding.frameColumnInts(long, java.lang.String, java.nio.Buffer, long, int, boolean, long, int) +method public abstract void dev.zudb.spi.ZuBinding.frameColumnStrings(long, java.lang.String, java.nio.Buffer, boolean, java.nio.Buffer, long) +method public abstract void dev.zudb.spi.ZuBinding.frameColumnViews(long, java.lang.String, java.nio.Buffer, java.util.List, long) +method public abstract void dev.zudb.spi.ZuBinding.frameFree(long) +method public abstract void dev.zudb.spi.ZuBinding.loaderColumnBooleans(long, java.lang.String, java.nio.IntBuffer) +method public abstract void dev.zudb.spi.ZuBinding.loaderColumnDoubles(long, java.lang.String, java.nio.DoubleBuffer) +method public abstract void dev.zudb.spi.ZuBinding.loaderColumnLongs(long, java.lang.String, java.nio.LongBuffer) +method public abstract void dev.zudb.spi.ZuBinding.loaderColumnStrings(long, java.lang.String, java.util.List) +method public abstract void dev.zudb.spi.ZuBinding.loaderColumnTemporal(long, java.lang.String, int, java.nio.LongBuffer) +method public abstract void dev.zudb.spi.ZuBinding.loaderEdges(long, java.nio.IntBuffer, java.nio.IntBuffer) +method public abstract void dev.zudb.spi.ZuBinding.loaderFinish(long) +method public abstract void dev.zudb.spi.ZuBinding.loaderFree(long) +method public abstract void dev.zudb.spi.ZuBinding.loaderTable(long, java.lang.String, java.lang.String, long) +method public abstract void dev.zudb.spi.ZuBinding.resultArrow(long, long, long, long) +method public abstract void dev.zudb.spi.ZuBinding.resultFree(long) +method public abstract void dev.zudb.spi.ZuBinding.rollback(long) +method public abstract void dev.zudb.spi.ZuBinding.stmtClose(long) +method public boolean dev.zudb.Appender.isFinished() +method public boolean dev.zudb.Config.readOnly() +method public boolean dev.zudb.Connection.inTransaction() +method public boolean dev.zudb.Connection.isClosed() +method public boolean dev.zudb.Connection.unregister(java.lang.String) +method public boolean dev.zudb.Database.isClosed() +method public boolean dev.zudb.Database.isMemory() +method public boolean dev.zudb.Diagnostic.retryable() +method public boolean dev.zudb.Frame.isClosed() +method public boolean dev.zudb.Loader.isFinished() +method public boolean dev.zudb.Result.isClosed() +method public boolean dev.zudb.Row.getBoolean(int) +method public boolean dev.zudb.Row.getBoolean(java.lang.String) +method public boolean dev.zudb.Row.isNull(int) +method public boolean dev.zudb.Row.isNull(java.lang.String) +method public boolean dev.zudb.Statement.isClosed() +method public boolean dev.zudb.Value$Bool.value() +method public boolean dev.zudb.ZuException.retryable() +method public dev.zudb.Appender dev.zudb.Appender.append(boolean) +method public dev.zudb.Appender dev.zudb.Appender.append(byte[]) +method public dev.zudb.Appender dev.zudb.Appender.append(dev.zudb.Value$Temporal$Kind, long) +method public dev.zudb.Appender dev.zudb.Appender.append(dev.zudb.Value$Temporal) +method public dev.zudb.Appender dev.zudb.Appender.append(double) +method public dev.zudb.Appender dev.zudb.Appender.append(java.lang.String) +method public dev.zudb.Appender dev.zudb.Appender.append(java.nio.ByteBuffer) +method public dev.zudb.Appender dev.zudb.Appender.append(java.time.Duration) +method public dev.zudb.Appender dev.zudb.Appender.append(java.time.LocalDate) +method public dev.zudb.Appender dev.zudb.Appender.append(java.time.LocalDateTime) +method public dev.zudb.Appender dev.zudb.Appender.append(java.time.LocalTime) +method public dev.zudb.Appender dev.zudb.Appender.append(java.time.Period) +method public dev.zudb.Appender dev.zudb.Appender.append(long) +method public dev.zudb.Appender dev.zudb.Appender.endRow() +method public dev.zudb.Appender dev.zudb.Appender.flush() +method public dev.zudb.Appender dev.zudb.Appender.row(java.lang.Object[]) +method public dev.zudb.Appender dev.zudb.Connection.appender(java.lang.String) +method public dev.zudb.Chunk dev.zudb.Result.chunk(long) +method public dev.zudb.Config dev.zudb.Config.with(java.lang.String, java.lang.String) +method public dev.zudb.Config dev.zudb.Config.withMemoryLimit(long) +method public dev.zudb.Config dev.zudb.Config.withReadOnly(boolean) +method public dev.zudb.Config dev.zudb.Config.withThreads(long) +method public dev.zudb.Connection dev.zudb.Connection.duplicate() +method public dev.zudb.Connection dev.zudb.Database.connect() +method public dev.zudb.Diagnostic dev.zudb.ZuException.diagnostic() +method public dev.zudb.Frame dev.zudb.Frame.booleans(java.lang.String, java.nio.ByteBuffer, long) +method public dev.zudb.Frame dev.zudb.Frame.column(java.lang.String, java.nio.DoubleBuffer) +method public dev.zudb.Frame dev.zudb.Frame.column(java.lang.String, java.nio.FloatBuffer) +method public dev.zudb.Frame dev.zudb.Frame.column(java.lang.String, java.nio.IntBuffer) +method public dev.zudb.Frame dev.zudb.Frame.column(java.lang.String, java.nio.LongBuffer) +method public dev.zudb.Frame dev.zudb.Frame.column(java.lang.String, java.nio.ShortBuffer) +method public dev.zudb.Frame dev.zudb.Frame.dates(java.lang.String, java.nio.IntBuffer) +method public dev.zudb.Frame dev.zudb.Frame.integers(java.lang.String, java.nio.Buffer, long, int, boolean, long, dev.zudb.Value$Temporal$Kind) +method public dev.zudb.Frame dev.zudb.Frame.strings(java.lang.String, java.nio.IntBuffer, java.nio.ByteBuffer) +method public dev.zudb.Frame dev.zudb.Frame.strings(java.lang.String, java.nio.LongBuffer, java.nio.ByteBuffer) +method public dev.zudb.Frame dev.zudb.Frame.timestamps(java.lang.String, java.nio.LongBuffer) +method public dev.zudb.Frame dev.zudb.Frame.views(java.lang.String, java.nio.ByteBuffer, java.util.List) +method public dev.zudb.Loader dev.zudb.Loader.booleanColumn(java.lang.String, java.nio.IntBuffer) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, boolean[]) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, double[]) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, java.lang.String[]) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, java.nio.DoubleBuffer) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, java.nio.LongBuffer) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, java.util.List) +method public dev.zudb.Loader dev.zudb.Loader.column(java.lang.String, long[]) +method public dev.zudb.Loader dev.zudb.Loader.edges(int[], int[]) +method public dev.zudb.Loader dev.zudb.Loader.edges(java.nio.IntBuffer, java.nio.IntBuffer) +method public dev.zudb.Loader dev.zudb.Loader.table(java.lang.String, java.lang.String, long) +method public dev.zudb.Loader dev.zudb.Loader.temporalColumn(java.lang.String, dev.zudb.Value$Temporal$Kind, java.nio.LongBuffer) +method public dev.zudb.Loader dev.zudb.Loader.temporalColumn(java.lang.String, dev.zudb.Value$Temporal$Kind, long[]) +method public dev.zudb.Result dev.zudb.Connection.query(java.lang.String) +method public dev.zudb.Result dev.zudb.Row.result() +method public dev.zudb.Result dev.zudb.Statement.execute() +method public dev.zudb.Row dev.zudb.Chunk.row(long) +method public dev.zudb.Row dev.zudb.Result.row(long) +method public dev.zudb.Severity dev.zudb.Diagnostic.severity() +method public dev.zudb.Severity dev.zudb.ZuException.severity() +method public dev.zudb.Statement dev.zudb.Connection.prepare(java.lang.String) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, boolean) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, dev.zudb.Value$Temporal$Kind, long, int) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, dev.zudb.Value$Temporal) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, double) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.lang.String) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.Duration) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.LocalDate) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.LocalDateTime) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.LocalTime) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.OffsetDateTime) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.OffsetTime) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, java.time.Period) +method public dev.zudb.Statement dev.zudb.Statement.bind(java.lang.String, long) +method public dev.zudb.Statement dev.zudb.Statement.bindNull(java.lang.String) +method public dev.zudb.Status dev.zudb.Diagnostic.status() +method public dev.zudb.Status dev.zudb.ZuException.status() +method public dev.zudb.Type dev.zudb.Result.cellType(long, int) +method public dev.zudb.Type dev.zudb.Row.type(int) +method public dev.zudb.Type dev.zudb.Row.type(java.lang.String) +method public dev.zudb.Value dev.zudb.Row.get(int) +method public dev.zudb.Value dev.zudb.Row.get(java.lang.String) +method public dev.zudb.Value dev.zudb.Value$Field.value() +method public dev.zudb.Value$Node dev.zudb.Row.getNode(int) +method public dev.zudb.Value$Node dev.zudb.Row.getNode(java.lang.String) +method public dev.zudb.Value$Temporal dev.zudb.Row.getTemporal(int) +method public dev.zudb.Value$Temporal dev.zudb.Row.getTemporal(java.lang.String) +method public dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal.kind() +method public dev.zudb.ZuException dev.zudb.Diagnostic.toException() +method public double dev.zudb.Row.getDouble(int) +method public double dev.zudb.Row.getDouble(java.lang.String) +method public double dev.zudb.Value$Float.value() +method public final boolean dev.zudb.Config.equals(java.lang.Object) +method public final boolean dev.zudb.Diagnostic.equals(java.lang.Object) +method public final boolean dev.zudb.Value$BindingTable.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Bool.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Field.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Float.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Graph.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Int.equals(java.lang.Object) +method public final boolean dev.zudb.Value$List.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Node.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Null.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Path.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Record.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Rel.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Str.equals(java.lang.Object) +method public final boolean dev.zudb.Value$Temporal.equals(java.lang.Object) +method public final boolean dev.zudb.ZuException$Position.equals(java.lang.Object) +method public final int dev.zudb.Config.hashCode() +method public final int dev.zudb.Diagnostic.hashCode() +method public final int dev.zudb.Value$BindingTable.hashCode() +method public final int dev.zudb.Value$Bool.hashCode() +method public final int dev.zudb.Value$Field.hashCode() +method public final int dev.zudb.Value$Float.hashCode() +method public final int dev.zudb.Value$Graph.hashCode() +method public final int dev.zudb.Value$Int.hashCode() +method public final int dev.zudb.Value$List.hashCode() +method public final int dev.zudb.Value$Node.hashCode() +method public final int dev.zudb.Value$Null.hashCode() +method public final int dev.zudb.Value$Path.hashCode() +method public final int dev.zudb.Value$Record.hashCode() +method public final int dev.zudb.Value$Rel.hashCode() +method public final int dev.zudb.Value$Str.hashCode() +method public final int dev.zudb.Value$Temporal.hashCode() +method public final int dev.zudb.ZuException$Position.hashCode() +method public final java.lang.String dev.zudb.Config.toString() +method public final java.lang.String dev.zudb.Diagnostic.toString() +method public final java.lang.String dev.zudb.Value$BindingTable.toString() +method public final java.lang.String dev.zudb.Value$Bool.toString() +method public final java.lang.String dev.zudb.Value$Field.toString() +method public final java.lang.String dev.zudb.Value$Float.toString() +method public final java.lang.String dev.zudb.Value$Graph.toString() +method public final java.lang.String dev.zudb.Value$Int.toString() +method public final java.lang.String dev.zudb.Value$List.toString() +method public final java.lang.String dev.zudb.Value$Node.toString() +method public final java.lang.String dev.zudb.Value$Null.toString() +method public final java.lang.String dev.zudb.Value$Path.toString() +method public final java.lang.String dev.zudb.Value$Record.toString() +method public final java.lang.String dev.zudb.Value$Rel.toString() +method public final java.lang.String dev.zudb.Value$Str.toString() +method public final java.lang.String dev.zudb.Value$Temporal.toString() +method public final java.lang.String dev.zudb.ZuException$Position.toString() +method public int dev.zudb.Appender.columns() +method public int dev.zudb.Diagnostic.column() +method public int dev.zudb.Diagnostic.line() +method public int dev.zudb.Diagnostic.offset() +method public int dev.zudb.Result.columnIndex(java.lang.String) +method public int dev.zudb.Result.columns() +method public int dev.zudb.Status.value() +method public int dev.zudb.Type.value() +method public int dev.zudb.Value$Node.table() +method public int dev.zudb.Value$Rel.table() +method public int dev.zudb.Value$Temporal$Kind.value() +method public int dev.zudb.Value$Temporal.offsetMinutes() +method public int dev.zudb.ZuException$Position.column() +method public int dev.zudb.ZuException$Position.line() +method public int dev.zudb.ZuException$Position.offset() +method public java.lang.String dev.zudb.Appender.columnName(int) +method public java.lang.String dev.zudb.Database.path() +method public java.lang.String dev.zudb.Diagnostic.code() +method public java.lang.String dev.zudb.Diagnostic.condition() +method public java.lang.String dev.zudb.Diagnostic.docUrl() +method public java.lang.String dev.zudb.Diagnostic.excerpt() +method public java.lang.String dev.zudb.Diagnostic.message() +method public java.lang.String dev.zudb.Frame.name() +method public java.lang.String dev.zudb.Result.columnName(int) +method public java.lang.String dev.zudb.Result.gqlstatus() +method public java.lang.String dev.zudb.Row.getString(int) +method public java.lang.String dev.zudb.Row.getString(java.lang.String) +method public java.lang.String dev.zudb.Row.toString() +method public java.lang.String dev.zudb.Value$Field.name() +method public java.lang.String dev.zudb.Value$Str.value() +method public java.nio.ByteBuffer dev.zudb.Chunk.valid(int) +method public java.nio.ByteBuffer dev.zudb.Result.valid(int) +method public java.nio.DoubleBuffer dev.zudb.Chunk.doubles(int) +method public java.nio.DoubleBuffer dev.zudb.Result.doubles(int) +method public java.nio.LongBuffer dev.zudb.Chunk.longs(int) +method public java.nio.LongBuffer dev.zudb.Chunk.nodeOffsets(int) +method public java.nio.LongBuffer dev.zudb.Result.longs(int) +method public java.nio.LongBuffer dev.zudb.Result.nodeOffsets(int) +method public java.time.Duration dev.zudb.Value$Temporal.toDuration() +method public java.time.LocalDate dev.zudb.Value$Temporal.toLocalDate() +method public java.time.LocalDateTime dev.zudb.Value$Temporal.toLocalDateTime() +method public java.time.LocalTime dev.zudb.Value$Temporal.toLocalTime() +method public java.time.OffsetDateTime dev.zudb.Value$Temporal.toOffsetDateTime() +method public java.time.OffsetTime dev.zudb.Value$Temporal.toOffsetTime() +method public java.time.Period dev.zudb.Value$Temporal.toPeriod() +method public java.time.ZoneOffset dev.zudb.Value$Temporal.offset() +method public java.util.Iterator dev.zudb.Result.iterator() +method public java.util.List dev.zudb.Result.notices() +method public java.util.List dev.zudb.Value$Record.fields() +method public java.util.List dev.zudb.Value$List.items() +method public java.util.List dev.zudb.Value$Path.items() +method public java.util.List dev.zudb.Connection.registeredNames() +method public java.util.List dev.zudb.Result.columnNames() +method public java.util.Optional dev.zudb.ZuException.position() +method public java.util.Optional dev.zudb.ZuException.caret() +method public java.util.Optional dev.zudb.ZuException.code() +method public java.util.Optional dev.zudb.ZuException.condition() +method public java.util.Optional dev.zudb.ZuException.docUrl() +method public java.util.Optional dev.zudb.ZuException.excerpt() +method public java.util.Spliterator dev.zudb.Result.spliterator() +method public java.util.stream.Stream dev.zudb.Result.chunks() +method public java.util.stream.Stream dev.zudb.Result.stream() +method public long dev.zudb.Appender.buffered() +method public long dev.zudb.Appender.committed() +method public long dev.zudb.Appender.discard() +method public long dev.zudb.Appender.finish() +method public long dev.zudb.Chunk.index() +method public long dev.zudb.Chunk.offset() +method public long dev.zudb.Chunk.rows() +method public long dev.zudb.Config.memoryLimit() +method public long dev.zudb.Config.threads() +method public long dev.zudb.Connection.registeredCount() +method public long dev.zudb.Connection.rowsRead() +method public long dev.zudb.Frame.rows() +method public long dev.zudb.Result.chunkCount() +method public long dev.zudb.Result.rows() +method public long dev.zudb.Row.getLong(int) +method public long dev.zudb.Row.getLong(java.lang.String) +method public long dev.zudb.Row.index() +method public long dev.zudb.Value$Int.value() +method public long dev.zudb.Value$Node.offset() +method public long dev.zudb.Value$Rel.source() +method public long dev.zudb.Value$Rel.target() +method public long dev.zudb.Value$Temporal.count() +method public static dev.zudb.Config dev.zudb.Config.defaults() +method public static dev.zudb.Config dev.zudb.Config.of(java.util.Map) +method public static dev.zudb.Connection dev.zudb.Connection.create(java.lang.String) +method public static dev.zudb.Connection dev.zudb.Connection.create(java.nio.file.Path) +method public static dev.zudb.Connection dev.zudb.Connection.memory() +method public static dev.zudb.Connection dev.zudb.Connection.open(java.lang.String) +method public static dev.zudb.Connection dev.zudb.Connection.open(java.nio.file.Path) +method public static dev.zudb.Database dev.zudb.Database.create(java.nio.file.Path) +method public static dev.zudb.Database dev.zudb.Database.create(java.nio.file.Path, dev.zudb.Config) +method public static dev.zudb.Database dev.zudb.Database.memory() +method public static dev.zudb.Database dev.zudb.Database.memory(dev.zudb.Config) +method public static dev.zudb.Database dev.zudb.Database.open(java.lang.String) +method public static dev.zudb.Database dev.zudb.Database.open(java.lang.String, dev.zudb.Config) +method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path) +method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path, dev.zudb.Config) +method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.misuse(dev.zudb.Status, java.lang.String) +method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.of(int, java.lang.String, java.lang.String, java.lang.String, int, int, int, int, java.lang.String, java.lang.String, boolean) +method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long) +method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long, java.lang.Runnable) +method public static dev.zudb.Loader dev.zudb.Loader.create(java.nio.file.Path) +method public static dev.zudb.Severity dev.zudb.Severity.valueOf(java.lang.String) +method public static dev.zudb.Severity[] dev.zudb.Severity.values() +method public static dev.zudb.Status dev.zudb.Status.of(int) +method public static dev.zudb.Status dev.zudb.Status.valueOf(java.lang.String) +method public static dev.zudb.Status[] dev.zudb.Status.values() +method public static dev.zudb.Type dev.zudb.Type.of(int) +method public static dev.zudb.Type dev.zudb.Type.valueOf(java.lang.String) +method public static dev.zudb.Type[] dev.zudb.Type.values() +method public static dev.zudb.Value$Null dev.zudb.Value$Null.instance() +method public static dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.of(int) +method public static dev.zudb.Value$Temporal$Kind dev.zudb.Value$Temporal$Kind.valueOf(java.lang.String) +method public static dev.zudb.Value$Temporal$Kind[] dev.zudb.Value$Temporal$Kind.values() +method public static java.lang.String dev.zudb.Zu.provider() +method public static java.lang.String dev.zudb.Zu.source() +method public static java.lang.String dev.zudb.Zu.version() +method public static java.lang.String dev.zudb.spi.Natives.flavour() +method public static java.lang.String dev.zudb.spi.Natives.platform() +method public static java.nio.file.Path dev.zudb.Zu.library() +method public static java.nio.file.Path dev.zudb.spi.Natives.unpack(java.lang.String, java.lang.String, java.lang.Class) +method public static java.util.Optional dev.zudb.Zu.availableProvider() +method public void dev.zudb.Appender.close() +method public void dev.zudb.Connection.begin() +method public void dev.zudb.Connection.beginReadOnly() +method public void dev.zudb.Connection.clearProgress() +method public void dev.zudb.Connection.close() +method public void dev.zudb.Connection.commit() +method public void dev.zudb.Connection.execute(java.lang.String) +method public void dev.zudb.Connection.interrupt() +method public void dev.zudb.Connection.onProgress(java.time.Duration, dev.zudb.Progress) +method public void dev.zudb.Connection.register(dev.zudb.Frame) +method public void dev.zudb.Connection.rollback() +method public void dev.zudb.Connection.transaction(java.lang.Runnable) +method public void dev.zudb.Database.close() +method public void dev.zudb.Frame.close() +method public void dev.zudb.Loader.close() +method public void dev.zudb.Loader.finish() +method public void dev.zudb.Result.close() +method public void dev.zudb.Result.exportArrow(long) +method public void dev.zudb.Result.exportArrow(long, long) +method public void dev.zudb.Statement.close() +type public abstract interface dev.zudb.Progress +type public abstract interface dev.zudb.Value sealed permits dev.zudb.Value$BindingTable, dev.zudb.Value$Bool, dev.zudb.Value$Float, dev.zudb.Value$Graph, dev.zudb.Value$Int, dev.zudb.Value$List, dev.zudb.Value$Node, dev.zudb.Value$Null, dev.zudb.Value$Path, dev.zudb.Value$Record, dev.zudb.Value$Rel, dev.zudb.Value$Str, dev.zudb.Value$Temporal +type public abstract interface dev.zudb.spi.ZuBinding +type public abstract interface dev.zudb.spi.ZuProvider +type public class dev.zudb.ZuClosedException extends dev.zudb.ZuProgrammingException +type public class dev.zudb.ZuConcurrentException extends dev.zudb.ZuProgrammingException +type public class dev.zudb.ZuConnectionException extends dev.zudb.ZuException +type public class dev.zudb.ZuDataException extends dev.zudb.ZuException +type public class dev.zudb.ZuException extends java.lang.RuntimeException +type public class dev.zudb.ZuInternalException extends dev.zudb.ZuException +type public class dev.zudb.ZuInterruptedException extends dev.zudb.ZuException +type public class dev.zudb.ZuProgrammingException extends dev.zudb.ZuException +type public class dev.zudb.ZuSyntaxException extends dev.zudb.ZuException +type public class dev.zudb.ZuTransactionException extends dev.zudb.ZuException +type public class dev.zudb.spi.ProviderUnavailableException extends java.lang.RuntimeException +type public final class dev.zudb.Appender implements java.lang.AutoCloseable +type public final class dev.zudb.Chunk +type public final class dev.zudb.Connection implements java.lang.AutoCloseable +type public final class dev.zudb.Database implements java.lang.AutoCloseable +type public final class dev.zudb.Frame implements java.lang.AutoCloseable +type public final class dev.zudb.Loader implements java.lang.AutoCloseable +type public final class dev.zudb.Result implements java.lang.AutoCloseable, java.lang.Iterable +type public final class dev.zudb.Row +type public final class dev.zudb.Statement implements java.lang.AutoCloseable +type public final class dev.zudb.Zu +type public final class dev.zudb.spi.Natives +type public final enum dev.zudb.Severity extends java.lang.Enum +type public final enum dev.zudb.Status extends java.lang.Enum +type public final enum dev.zudb.Type extends java.lang.Enum +type public final record dev.zudb.Config extends java.lang.Record +type public final record dev.zudb.Diagnostic extends java.lang.Record +type public static final enum dev.zudb.Value$Temporal$Kind extends java.lang.Enum +type public static final record dev.zudb.Value$BindingTable extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Bool extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Field extends java.lang.Record +type public static final record dev.zudb.Value$Float extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Graph extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Int extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$List extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Node extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Null extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Path extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Record extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Rel extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Str extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Temporal extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.ZuException$Position extends java.lang.Record diff --git a/zudb/src/test/java/dev/zudb/SurfaceTest.java b/zudb/src/test/java/dev/zudb/SurfaceTest.java new file mode 100644 index 0000000..3784c38 --- /dev/null +++ b/zudb/src/test/java/dev/zudb/SurfaceTest.java @@ -0,0 +1,324 @@ +package dev.zudb; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.TypeVariable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +/** + * The public surface, written down, so that moving it is something a reviewer + * sees. + * + *

A client's API is a promise, and the way that promise gets broken is + * never a decision anybody announced. It is a parameter that became an + * {@code Optional}, a class that stopped being public when it moved package, a + * method that gained an overload and made an existing call ambiguous. Each of + * those is one line in a diff that looks like an implementation change, and by + * the time a user finds out the release has gone. + * + *

So the surface lives in {@code api/surface.txt} at the root of the + * repository, one line per type and per member, and this test regenerates it + * and compares. A change to the API is then a change to that file, in the same + * commit, where it is the first thing in the diff rather than the last. That + * is the whole mechanism, and it is the same one zu-go uses, which matters + * because a reviewer moving between the clients should not have to learn a + * second one. + * + *

Run with {@code -Dzu.surface.write=true} to write the file instead of + * checking it. That is the one command to reach for when the change was + * meant, and running it is how you say so. + */ +class SurfaceTest { + + /** Where the surface is written down, from this module's directory. */ + private static final Path SURFACE = Paths.get("..", "api", "surface.txt"); + + /** The packages this module exports, which are the whole of the promise. */ + private static final List EXPORTED = List.of("dev.zudb", "dev.zudb.spi"); + + @Test + void theSurfaceIsWhatTheFileSaysItIs() throws IOException { + String found = surface(); + if (Boolean.getBoolean("zu.surface.write")) { + Files.createDirectories(SURFACE.getParent()); + Files.writeString(SURFACE, found, StandardCharsets.UTF_8); + return; + } + assertTrue( + Files.isRegularFile(SURFACE), + SURFACE.toAbsolutePath().normalize() + + " is missing: write it with mvn -pl zudb test -Dzu.surface.write=true"); + String written = Files.readString(SURFACE, StandardCharsets.UTF_8); + if (written.equals(found)) { + return; + } + // Not assertEquals on the whole thing. Five hundred lines printed twice + // is a report nobody reads, and the four lines that moved are the report. + TreeSet was = new TreeSet<>(written.lines().toList()); + TreeSet is = new TreeSet<>(found.lines().toList()); + StringBuilder moved = new StringBuilder(); + for (String line : is) { + if (!was.contains(line)) { + moved.append("\n + ").append(line); + } + } + for (String line : was) { + if (!is.contains(line)) { + moved.append("\n - ").append(line); + } + } + throw new AssertionError( + "the public surface moved and api/surface.txt did not:" + + moved + + "\n\nA name that arrived is a minor release. A name that went or changed shape is a" + + " major one, or a mistake. If it was meant, write the file down in the same commit" + + " with mvn -pl zudb test -Dzu.surface.write=true and say in the message which of" + + " those it is. If it was not, this is the review catching it."); + } + + @Test + void theSurfaceIsNotEmpty() throws IOException { + // A generator that quietly found nothing would make every future change + // pass, which is the one failure this test cannot report on itself. + long lines = surface().lines().count(); + assertTrue(lines > 300, "the surface came out at " + lines + " lines, which is not a client"); + } + + /** Every exported type and member, one a line, sorted. */ + private static String surface() throws IOException { + TreeSet lines = new TreeSet<>(); + for (Class type : types()) { + lines.add("type " + declaration(type)); + for (Field field : type.getDeclaredFields()) { + if (published(field.getModifiers()) && !field.isSynthetic()) { + lines.add( + "field " + + modifiers(field.getModifiers() & Modifier.fieldModifiers()) + + field.getGenericType().getTypeName() + + " " + + field.getDeclaringClass().getTypeName() + + "." + + field.getName()); + } + } + for (Constructor ctor : type.getDeclaredConstructors()) { + if (published(ctor.getModifiers()) && !ctor.isSynthetic()) { + lines.add( + "ctor " + + modifiers(ctor.getModifiers() & Modifier.constructorModifiers()) + + generics(ctor.getTypeParameters()) + + ctor.getDeclaringClass().getTypeName() + + arguments(ctor.getGenericParameterTypes()) + + thrown(ctor.getGenericExceptionTypes())); + } + } + for (Method method : type.getDeclaredMethods()) { + if (published(method.getModifiers()) && !method.isSynthetic() && !method.isBridge()) { + lines.add( + "method " + + modifiers(method.getModifiers() & Modifier.methodModifiers()) + + generics(method.getTypeParameters()) + + method.getGenericReturnType().getTypeName() + + " " + + method.getDeclaringClass().getTypeName() + + "." + + method.getName() + + arguments(method.getGenericParameterTypes()) + + thrown(method.getGenericExceptionTypes())); + } + } + } + StringBuilder out = new StringBuilder(); + for (String line : lines) { + out.append(line).append('\n'); + } + return out.toString(); + } + + /** The modifiers of a type that a caller outside the module can see. */ + private static final int MODIFIERS = + Modifier.PUBLIC | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.ABSTRACT; + + /** + * What a type is, spelled out here rather than taken from + * {@code toGenericString}. + * + *

That method is not the same on every JDK this client is built for. JDK + * 21 leaves the sealed marker out of it and JDK 25 puts it in, so a file + * written on one and checked on the other reports a change nobody made, and + * a gate that cries wolf on the version matrix is a gate people turn off. + * Everything here comes off the class file instead. + * + *

Sealing and the list of permitted subclasses are part of the promise, + * and so is the interface a type implements, which is how a caller knows a + * {@link Result} works in a for-each. + */ + private static String declaration(Class type) { + StringBuilder out = new StringBuilder(); + String modifiers = Modifier.toString(type.getModifiers() & MODIFIERS); + if (!modifiers.isEmpty()) { + out.append(modifiers).append(' '); + } + out.append(kind(type)).append(' ').append(type.getName()).append(parameters(type)); + if (type.isSealed()) { + TreeSet permitted = new TreeSet<>(); + for (Class each : type.getPermittedSubclasses()) { + permitted.add(each.getTypeName()); + } + out.append(" sealed permits ").append(String.join(", ", permitted)); + } + if (type.getSuperclass() != null && type.getSuperclass() != Object.class) { + out.append(" extends ").append(type.getGenericSuperclass().getTypeName()); + } + TreeSet faces = new TreeSet<>(); + for (java.lang.reflect.Type each : type.getGenericInterfaces()) { + faces.add(each.getTypeName()); + } + if (!faces.isEmpty()) { + out.append(type.isInterface() ? " extends " : " implements ") + .append(String.join(", ", faces)); + } + return out.toString(); + } + + /** Which of the five a type is. */ + private static String kind(Class type) { + if (type.isAnnotation()) { + return "@interface"; + } + if (type.isInterface()) { + return "interface"; + } + if (type.isEnum()) { + return "enum"; + } + if (type.isRecord()) { + return "record"; + } + return "class"; + } + + /** Modifiers with a space after them, or nothing at all. */ + private static String modifiers(int bits) { + String words = Modifier.toString(bits); + return words.isEmpty() ? "" : words + " "; + } + + /** The argument list of a constructor or a method, brackets and all. */ + private static String arguments(java.lang.reflect.Type[] args) { + List each = new ArrayList<>(); + for (java.lang.reflect.Type arg : args) { + each.add(arg.getTypeName()); + } + return "(" + String.join(", ", each) + ")"; + } + + /** What a member declares it throws, or nothing at all. */ + private static String thrown(java.lang.reflect.Type[] exceptions) { + if (exceptions.length == 0) { + return ""; + } + List each = new ArrayList<>(); + for (java.lang.reflect.Type exception : exceptions) { + each.add(exception.getTypeName()); + } + return " throws " + String.join(", ", each); + } + + /** The type parameters a member declares, with a space after them. */ + private static String generics(TypeVariable[] vars) { + String written = parameters(vars); + return written.isEmpty() ? "" : written + " "; + } + + /** The type parameters a type declares, or nothing at all. */ + private static String parameters(Class type) { + return parameters(type.getTypeParameters()); + } + + /** The type parameters a type or a member declares, or nothing at all. */ + private static String parameters(TypeVariable[] vars) { + if (vars.length == 0) { + return ""; + } + List each = new ArrayList<>(); + for (TypeVariable var : vars) { + StringBuilder one = new StringBuilder(var.getName()); + List bounds = new ArrayList<>(); + for (java.lang.reflect.Type bound : var.getBounds()) { + if (bound != Object.class) { + bounds.add(bound.getTypeName()); + } + } + if (!bounds.isEmpty()) { + one.append(" extends ").append(String.join(" & ", bounds)); + } + each.add(one.toString()); + } + return "<" + String.join(", ", each) + ">"; + } + + /** Whether a member is one a caller outside this module can name. */ + private static boolean published(int modifiers) { + return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); + } + + /** + * The exported types, read off the classes this module was compiled to. + * + *

Reading the directory rather than a list of names is the point: a type + * somebody adds turns up here without anybody remembering to add it, which + * is the failure mode of every hand-kept inventory. + */ + private static List> types() throws IOException { + Path classes = Paths.get("target", "classes"); + assertTrue( + Files.isDirectory(classes), + classes.toAbsolutePath().normalize() + " is missing: the build writes it before the tests"); + List> found = new ArrayList<>(); + try (Stream walk = Files.walk(classes)) { + for (Path each : walk.toList()) { + String name = classes.relativize(each).toString(); + if (!name.endsWith(".class")) { + continue; + } + name = + name.substring(0, name.length() - ".class".length()) + .replace(java.io.File.separatorChar, '.') + .replace('/', '.'); + if (name.equals("module-info") || name.endsWith("package-info")) { + continue; + } + String pkg = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : ""; + if (!EXPORTED.contains(pkg)) { + continue; + } + Class type; + try { + type = Class.forName(name, false, SurfaceTest.class.getClassLoader()); + } catch (ClassNotFoundException cannot) { + throw new IllegalStateException(name + " is on disk and not loadable", cannot); + } + if (published(type.getModifiers())) { + found.add(type); + } + } + } + assertTrue(found.size() > 20, "found " + found.size() + " exported types, which is too few"); + found.sort((a, b) -> a.getName().compareTo(b.getName())); + return found; + } +}