From 22be0c2cb8a5e3d30d1dc74118989931786126c7 Mon Sep 17 00:00:00 2001 From: jorgee Date: Tue, 13 May 2025 15:42:26 +0200 Subject: [PATCH 1/7] add spec field to json model Signed-off-by: jorgee --- .../serde/gson/RuntimeTypeAdapterFactory.java | 13 ++++++ .../serde/LinTypeAdapterFactory.groovy | 46 ++++++++----------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java b/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java index fe51535f60..1587147104 100644 --- a/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java +++ b/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java @@ -251,6 +251,19 @@ public RuntimeTypeAdapterFactory registerSubtype(Class type) { return registerSubtype(type, type.getSimpleName()); } + protected Class getSubTypeFromLabel(String label){ + return labelToSubtype.get(label); + } + + protected String getLabelFromSubtype(Class subType){ + return subtypeToLabel.get(subType); + } + + protected String getTypeFieldName(){ + return typeFieldName; + } + + @Override public TypeAdapter create(Gson gson, TypeToken type) { if (type == null) { diff --git a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy index 7e6cba7b7f..f31014704f 100644 --- a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy +++ b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy @@ -43,8 +43,10 @@ import nextflow.serde.gson.RuntimeTypeAdapterFactory @CompileStatic class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { public static final String VERSION_FIELD = 'version' + public static final String SPEC_FIELD = 'spec' public static final String CURRENT_VERSION = LinModel.VERSION + private labelToClass = [:] LinTypeAdapterFactory() { super(LinSerializable.class, "kind", false) this.registerSubtype(WorkflowRun, WorkflowRun.simpleName) @@ -70,39 +72,31 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { return new TypeAdapter() { @Override void write(JsonWriter out, R value) throws IOException { - def json = delegate.toJsonTree(value) - if (json instanceof JsonObject) { - json = addVersion(json) - } - gson.toJson(json, out) + final object = new JsonObject() + object.addProperty(VERSION_FIELD, CURRENT_VERSION) + String label = getLabelFromSubtype(value.class) + if (!label) + throw new JsonParseException("Not registered class ${value.class}") + object.addProperty(getTypeFieldName(), label) + def json = gson.toJsonTree(value) + object.add(SPEC_FIELD, json) + gson.toJson(object, out) } @Override R read(JsonReader reader) throws IOException { - def json = JsonParser.parseReader(reader) - if (json instanceof JsonObject) { - def obj = (JsonObject) json - def versionEl = obj.get(VERSION_FIELD) - if (versionEl == null || versionEl.asString != CURRENT_VERSION) { - throw new JsonParseException("Invalid or missing version") - } - obj.remove(VERSION_FIELD) + def json = JsonParser.parseReader(reader).asJsonObject + def obj = (JsonObject) json + def versionEl = obj.get(VERSION_FIELD) + if (versionEl == null || versionEl.asString != CURRENT_VERSION) { + throw new JsonParseException("Invalid or missing version") } - return delegate.fromJsonTree(json) + final typeEl = obj.get(getTypeFieldName()) + final specEl = obj.get(SPEC_FIELD).asJsonObject + specEl.add(getTypeFieldName(), typeEl) + return delegate.fromJsonTree(specEl) } } } - private static JsonObject addVersion(JsonObject json){ - if( json.has(VERSION_FIELD) ) - throw new JsonParseException("object already defines a field named ${VERSION_FIELD}") - - JsonObject clone = new JsonObject(); - clone.addProperty(VERSION_FIELD, CURRENT_VERSION) - for (Map.Entry e : json.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - return clone - } - } From 0ad076c374ec434bf968836ae675ce83fa1b810b Mon Sep 17 00:00:00 2001 From: jorgee Date: Wed, 14 May 2025 10:31:36 +0200 Subject: [PATCH 2/7] Fix tests Signed-off-by: jorgee --- .../serde/LinTypeAdapterFactory.groovy | 13 +++++-- .../lineage/cli/LinCommandImplTest.groovy | 38 +++++++++---------- .../fs/LinFileSystemProviderTest.groovy | 14 +++---- .../nextflow/lineage/fs/LinPathTest.groovy | 6 +-- .../lineage/serde/LinEncoderTest.groovy | 2 +- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy index f31014704f..52efbb13e8 100644 --- a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy +++ b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy @@ -85,14 +85,19 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { @Override R read(JsonReader reader) throws IOException { - def json = JsonParser.parseReader(reader).asJsonObject - def obj = (JsonObject) json + def obj = JsonParser.parseReader(reader)?.asJsonObject + if( !obj ) + throw new JsonParseException("Parsed object is null") def versionEl = obj.get(VERSION_FIELD) if (versionEl == null || versionEl.asString != CURRENT_VERSION) { - throw new JsonParseException("Invalid or missing version") + throw new JsonParseException("'Invalid or missing '${VERSION_FIELD}' property") } final typeEl = obj.get(getTypeFieldName()) - final specEl = obj.get(SPEC_FIELD).asJsonObject + if( !typeEl ) + throw new JsonParseException("'${getTypeFieldName()}' not found") + final specEl = obj.get(SPEC_FIELD)?.asJsonObject + if ( !specEl ) + throw new JsonParseException("'Invalid or missing '${SPEC_FIELD}' property") specEl.add(getTypeFieldName(), typeEl) return delegate.fromJsonTree(specEl) } diff --git a/modules/nf-lineage/src/test/nextflow/lineage/cli/LinCommandImplTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/cli/LinCommandImplTest.groovy index 1a966ad2be..a1702cb94e 100644 --- a/modules/nf-lineage/src/test/nextflow/lineage/cli/LinCommandImplTest.groovy +++ b/modules/nf-lineage/src/test/nextflow/lineage/cli/LinCommandImplTest.groovy @@ -387,27 +387,27 @@ class LinCommandImplTest extends Specification{ def expectedOutput = '''diff --git 12345 67890 --- 12345 +++ 67890 -@@ -1,16 +1,16 @@ - { +@@ -2,16 +2,16 @@ "version": "lineage/v1beta1", "kind": "FileOutput", -- "path": "path/to/file", -+ "path": "path/to/file2", - "checksum": { -- "value": "45372qe", -+ "value": "42472qet", - "algorithm": "nextflow", - "mode": "standard" - }, -- "source": "lid://123987/file.bam", -+ "source": "lid://123987/file2.bam", - "workflowRun": "lid://123987/", - "taskRun": null, -- "size": 1234, -+ "size": 1235, - "createdAt": "1970-01-02T10:17:36.789Z", - "modifiedAt": "1970-01-02T10:17:36.789Z", - "labels": null + "spec": { +- "path": "path/to/file", ++ "path": "path/to/file2", + "checksum": { +- "value": "45372qe", ++ "value": "42472qet", + "algorithm": "nextflow", + "mode": "standard" + }, +- "source": "lid://123987/file.bam", ++ "source": "lid://123987/file2.bam", + "workflowRun": "lid://123987/", + "taskRun": null, +- "size": 1234, ++ "size": 1235, + "createdAt": "1970-01-02T10:17:36.789Z", + "modifiedAt": "1970-01-02T10:17:36.789Z", + "labels": null ''' when: diff --git a/modules/nf-lineage/src/test/nextflow/lineage/fs/LinFileSystemProviderTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/fs/LinFileSystemProviderTest.groovy index 1800e7b393..2d5e83194d 100644 --- a/modules/nf-lineage/src/test/nextflow/lineage/fs/LinFileSystemProviderTest.groovy +++ b/modules/nf-lineage/src/test/nextflow/lineage/fs/LinFileSystemProviderTest.groovy @@ -123,7 +123,7 @@ class LinFileSystemProviderTest extends Specification { def output = data.resolve("output.txt") output.text = "Hello, World!" outputMeta.mkdirs() - outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"FileOutput","path":"'+output.toString()+'"}' + outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path":"'+output.toString()+'"}}' Global.session = Mock(Session) { getConfig()>>config } and: @@ -179,7 +179,7 @@ class LinFileSystemProviderTest extends Specification { def config = [lineage:[store:[location:wdir.toString()]]] def outputMeta = wdir.resolve("12345") outputMeta.mkdirs() - outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"WorkflowRun","sessionId":"session","name":"run_name","params":[{"type":"String","name":"param1","value":"value1"}]}' + outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"WorkflowRun","spec":{"sessionId":"session","name":"run_name","params":[{"type":"String","name":"param1","value":"value1"}]}}' Global.session = Mock(Session) { getConfig()>>config } and: @@ -238,7 +238,7 @@ class LinFileSystemProviderTest extends Specification { def output = data.resolve("output.txt") output.text = "Hello, World!" outputMeta.mkdirs() - outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"FileOutput","path":"'+output.toString()+'"}' + outputMeta.resolve(".data.json").text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path":"'+output.toString()+'"}}' Global.session = Mock(Session) { getConfig()>>config } and: @@ -278,8 +278,8 @@ class LinFileSystemProviderTest extends Specification { output1.resolve('file3.txt').text = 'file3' wdir.resolve('12345/output1').mkdirs() wdir.resolve('12345/output2').mkdirs() - wdir.resolve('12345/.data.json').text = '{"version":"lineage/v1beta1","kind":"TaskRun"}' - wdir.resolve('12345/output1/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput", "path": "' + output1.toString() + '"}' + wdir.resolve('12345/.data.json').text = '{"version":"lineage/v1beta1","kind":"TaskRun","spec":{"name":"dummy"}}' + wdir.resolve('12345/output1/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path": "' + output1.toString() + '"}}' and: def config = [lineage:[store:[location:wdir.toString()]]] @@ -403,7 +403,7 @@ class LinFileSystemProviderTest extends Specification { output.resolve('abc').text = 'file1' output.resolve('.foo').text = 'file2' wdir.resolve('12345/output').mkdirs() - wdir.resolve('12345/output/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput", "path": "' + output.toString() + '"}' + wdir.resolve('12345/output/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path": "' + output.toString() + '"}}' and: def provider = new LinFileSystemProvider() def lid1 = provider.getPath(LinPath.asUri('lid://12345/output/abc')) @@ -423,7 +423,7 @@ class LinFileSystemProviderTest extends Specification { def file = data.resolve('abc') file.text = 'Hello' wdir.resolve('12345/abc').mkdirs() - wdir.resolve('12345/abc/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput", "path":"' + file.toString() + '"}' + wdir.resolve('12345/abc/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path":"' + file.toString() + '"}}' and: Global.session = Mock(Session) { getConfig()>>config } and: diff --git a/modules/nf-lineage/src/test/nextflow/lineage/fs/LinPathTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/fs/LinPathTest.groovy index fdb16cf5f8..b7ec578655 100644 --- a/modules/nf-lineage/src/test/nextflow/lineage/fs/LinPathTest.groovy +++ b/modules/nf-lineage/src/test/nextflow/lineage/fs/LinPathTest.groovy @@ -161,9 +161,9 @@ class LinPathTest extends Specification { wdir.resolve('12345/output1').mkdirs() wdir.resolve('12345/path/to/file2.txt').mkdirs() - wdir.resolve('12345/.data.json').text = '{"version":"lineage/v1beta1","kind":"TaskRun"}' - wdir.resolve('12345/output1/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput", "path": "' + outputFolder.toString() + '"}' - wdir.resolve('12345/path/to/file2.txt/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput", "path": "' + outputFile.toString() + '"}' + wdir.resolve('12345/.data.json').text = '{"version":"lineage/v1beta1","kind":"TaskRun","spec":{"name":"test"}}' + wdir.resolve('12345/output1/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path": "' + outputFolder.toString() + '"}}' + wdir.resolve('12345/path/to/file2.txt/.data.json').text = '{"version":"lineage/v1beta1","kind":"FileOutput","spec":{"path": "' + outputFile.toString() + '"}}' def time = OffsetDateTime.now() def wfResultsMetadata = new LinEncoder().withPrettyPrint(true).encode(new WorkflowOutput(time, "lid://1234", [new Parameter( "Path", "a", "lid://1234/a.txt")])) wdir.resolve('5678/').mkdirs() diff --git a/modules/nf-lineage/src/test/nextflow/lineage/serde/LinEncoderTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinEncoderTest.groovy index aab4738d63..85d77d6acc 100644 --- a/modules/nf-lineage/src/test/nextflow/lineage/serde/LinEncoderTest.groovy +++ b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinEncoderTest.groovy @@ -163,7 +163,7 @@ class LinEncoderTest extends Specification{ def encoded = encoder.encode(wfResults) def object = encoder.decode(encoded) then: - encoded == '{"version":"lineage/v1beta1","kind":"WorkflowOutput","createdAt":null,"workflowRun":"lid://1234","output":null}' + encoded == '{"version":"lineage/v1beta1","kind":"WorkflowOutput","spec":{"createdAt":null,"workflowRun":"lid://1234","output":null}}' def result = object as WorkflowOutput result.createdAt == null From 9f7c0fcae5288275e6bd814d072916f886f04c27 Mon Sep 17 00:00:00 2001 From: Jorge Ejarque Date: Thu, 15 May 2025 10:04:46 +0200 Subject: [PATCH 3/7] Update typo in message [ci fast] Co-authored-by: Paolo Di Tommaso Signed-off-by: Jorge Ejarque --- .../main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy index 52efbb13e8..00ce6b2416 100644 --- a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy +++ b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy @@ -90,7 +90,7 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { throw new JsonParseException("Parsed object is null") def versionEl = obj.get(VERSION_FIELD) if (versionEl == null || versionEl.asString != CURRENT_VERSION) { - throw new JsonParseException("'Invalid or missing '${VERSION_FIELD}' property") + throw new JsonParseException("Invalid or missing '${VERSION_FIELD}' property") } final typeEl = obj.get(getTypeFieldName()) if( !typeEl ) From 1ca9f43667dec280313e31dd94658b8fe51faf4f Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 8 Jul 2025 19:30:10 +0200 Subject: [PATCH 4/7] Minor change [ci fast] Signed-off-by: Paolo Di Tommaso --- .../serde/LinTypeAdapterFactory.groovy | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy index 00ce6b2416..fb531a9bc9 100644 --- a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy +++ b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy @@ -16,7 +16,6 @@ package nextflow.lineage.serde import com.google.gson.Gson -import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParseException import com.google.gson.JsonParser @@ -24,7 +23,6 @@ import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter - import groovy.transform.CompileStatic import nextflow.lineage.model.v1beta1.FileOutput import nextflow.lineage.model.v1beta1.LinModel @@ -34,7 +32,6 @@ import nextflow.lineage.model.v1beta1.Workflow import nextflow.lineage.model.v1beta1.WorkflowOutput import nextflow.lineage.model.v1beta1.WorkflowRun import nextflow.serde.gson.RuntimeTypeAdapterFactory - /** * Class to serialize LiSerializable objects including the Lineage model version. * @@ -42,11 +39,11 @@ import nextflow.serde.gson.RuntimeTypeAdapterFactory */ @CompileStatic class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { + public static final String VERSION_FIELD = 'version' public static final String SPEC_FIELD = 'spec' public static final String CURRENT_VERSION = LinModel.VERSION - private labelToClass = [:] LinTypeAdapterFactory() { super(LinSerializable.class, "kind", false) this.registerSubtype(WorkflowRun, WorkflowRun.simpleName) @@ -55,7 +52,6 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { .registerSubtype(TaskRun, TaskRun.simpleName) .registerSubtype(TaskOutput, TaskOutput.simpleName) .registerSubtype(FileOutput, FileOutput.simpleName) - } @Override @@ -85,21 +81,21 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { @Override R read(JsonReader reader) throws IOException { - def obj = JsonParser.parseReader(reader)?.asJsonObject - if( !obj ) - throw new JsonParseException("Parsed object is null") - def versionEl = obj.get(VERSION_FIELD) + final obj = JsonParser.parseReader(reader)?.getAsJsonObject() + if( obj==null ) + throw new JsonParseException("Parsed JSON object is null") + final versionEl = obj.get(VERSION_FIELD) if (versionEl == null || versionEl.asString != CURRENT_VERSION) { - throw new JsonParseException("Invalid or missing '${VERSION_FIELD}' property") + throw new JsonParseException("Invalid or missing '${VERSION_FIELD}' JSON property") } final typeEl = obj.get(getTypeFieldName()) - if( !typeEl ) - throw new JsonParseException("'${getTypeFieldName()}' not found") + if( typeEl==null ) + throw new JsonParseException("JSON property '${getTypeFieldName()}' not found") final specEl = obj.get(SPEC_FIELD)?.asJsonObject - if ( !specEl ) - throw new JsonParseException("'Invalid or missing '${SPEC_FIELD}' property") + if ( specEl==null ) + throw new JsonParseException("Invalid or missing '${SPEC_FIELD}' JSON property") specEl.add(getTypeFieldName(), typeEl) - return delegate.fromJsonTree(specEl) + return (R) delegate.fromJsonTree(specEl) } } } From f52d7a81ca9b96a3c7811b2fdf34673c7ec68430 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 8 Jul 2025 20:52:52 +0200 Subject: [PATCH 5/7] Add backward compatibility Signed-off-by: Paolo Di Tommaso --- .../serde/LinTypeAdapterFactory.groovy | 15 +- .../serde/LinTypeAdapterFactoryTest.groovy | 181 ++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy diff --git a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy index fb531a9bc9..6d0516a232 100644 --- a/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy +++ b/modules/nf-lineage/src/main/nextflow/lineage/serde/LinTypeAdapterFactory.groovy @@ -91,11 +91,18 @@ class LinTypeAdapterFactory extends RuntimeTypeAdapterFactory { final typeEl = obj.get(getTypeFieldName()) if( typeEl==null ) throw new JsonParseException("JSON property '${getTypeFieldName()}' not found") + + // Check if this is the new format (has 'spec' field) or old format (data at root level) final specEl = obj.get(SPEC_FIELD)?.asJsonObject - if ( specEl==null ) - throw new JsonParseException("Invalid or missing '${SPEC_FIELD}' JSON property") - specEl.add(getTypeFieldName(), typeEl) - return (R) delegate.fromJsonTree(specEl) + if ( specEl != null ) { + // New format: data is wrapped in 'spec' field + specEl.add(getTypeFieldName(), typeEl) + return (R) delegate.fromJsonTree(specEl) + } else { + // Old format: data is at root level, just remove version field + obj.remove(VERSION_FIELD) + return (R) delegate.fromJsonTree(obj) + } } } } diff --git a/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy new file mode 100644 index 0000000000..a2a376d1b7 --- /dev/null +++ b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy @@ -0,0 +1,181 @@ +/* + * Copyright 2013-2025, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.lineage.serde + +import nextflow.lineage.model.v1beta1.Checksum +import nextflow.lineage.model.v1beta1.FileOutput +import nextflow.lineage.model.v1beta1.LinModel +import nextflow.lineage.model.v1beta1.TaskRun +import spock.lang.Specification + +class LinTypeAdapterFactoryTest extends Specification { + + def 'should read both new and old JSON formats'() { + given: + def encoder = new LinEncoder() + + when: 'create old format JSON (without spec wrapper)' + def oldFormatJson = """ + { + "version": "${LinModel.VERSION}", + "kind": "FileOutput", + "path": "/path/to/file", + "checksum": { + "value": "hash_value", + "algorithm": "hash_algorithm", + "mode": "standard" + }, + "source": "lid://source", + "workflow": "lid://workflow", + "task": "lid://task", + "size": 1234 + } + """ + + and: 'deserialize old format' + def oldResult = encoder.decode(oldFormatJson) + + then: 'should work correctly with backward compatibility' + oldResult instanceof FileOutput + oldResult.path == "/path/to/file" + oldResult.checksum.value == "hash_value" + oldResult.source == "lid://source" + oldResult.size == 1234 + + when: 'create new format JSON (with spec wrapper)' + def newFormatJson = """ + { + "version": "${LinModel.VERSION}", + "kind": "FileOutput", + "spec": { + "path": "/path/to/file", + "checksum": { + "value": "hash_value", + "algorithm": "hash_algorithm", + "mode": "standard" + }, + "source": "lid://source", + "workflow": "lid://workflow", + "task": "lid://task", + "size": 1234 + } + } + """ + + and: 'deserialize new format' + def newResult = encoder.decode(newFormatJson) + + then: 'should work correctly' + newResult instanceof FileOutput + newResult.path == "/path/to/file" + newResult.checksum.value == "hash_value" + newResult.source == "lid://source" + newResult.size == 1234 + } + + def 'should handle TaskRun old format'() { + given: + def encoder = new LinEncoder() + + when: 'create old format TaskRun JSON' + def oldFormatJson = """ + { + "version": "${LinModel.VERSION}", + "kind": "TaskRun", + "sessionId": "session123", + "name": "testTask", + "codeChecksum": { + "value": "hash123", + "algorithm": "nextflow", + "mode": "standard" + }, + "script": "echo hello", + "input": [], + "container": "ubuntu:latest", + "conda": null, + "spack": null, + "architecture": "amd64", + "globalVars": {}, + "binEntries": [] + } + """ + + and: 'deserialize old format' + def result = encoder.decode(oldFormatJson) + + then: 'should work correctly' + result instanceof TaskRun + result.sessionId == "session123" + result.name == "testTask" + result.codeChecksum.value == "hash123" + result.script == "echo hello" + result.container == "ubuntu:latest" + result.architecture == "amd64" + } + + def 'should reject JSON without version field'() { + given: + def encoder = new LinEncoder() + + when: 'try to deserialize JSON without version' + def invalidJson = """ + { + "kind": "FileOutput", + "path": "/path/to/file" + } + """ + encoder.decode(invalidJson) + + then: 'should throw exception' + thrown(Exception) + } + + def 'should reject JSON with wrong version'() { + given: + def encoder = new LinEncoder() + + when: 'try to deserialize JSON with wrong version' + def invalidJson = """ + { + "version": "wrong/version", + "kind": "FileOutput", + "path": "/path/to/file" + } + """ + encoder.decode(invalidJson) + + then: 'should throw exception' + thrown(Exception) + } + + def 'should reject JSON without kind field'() { + given: + def encoder = new LinEncoder() + + when: 'try to deserialize JSON without kind' + def invalidJson = """ + { + "version": "${LinModel.VERSION}", + "path": "/path/to/file" + } + """ + encoder.decode(invalidJson) + + then: 'should throw exception' + thrown(Exception) + } +} \ No newline at end of file From 64b3f0b2dd74f41710a5394f69533e27f8e3b45c Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 8 Jul 2025 20:55:30 +0200 Subject: [PATCH 6/7] Update modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java [ci skip] Signed-off-by: Paolo Di Tommaso --- .../src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java b/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java index 1587147104..90d14f2954 100644 --- a/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java +++ b/modules/nf-commons/src/main/nextflow/serde/gson/RuntimeTypeAdapterFactory.java @@ -263,7 +263,6 @@ protected String getTypeFieldName(){ return typeFieldName; } - @Override public TypeAdapter create(Gson gson, TypeToken type) { if (type == null) { From 7584275932ef4f13740fc24257a3eecd0f001ab5 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 8 Jul 2025 20:56:25 +0200 Subject: [PATCH 7/7] Add new line [ci skip] Signed-off-by: Paolo Di Tommaso --- .../nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy index a2a376d1b7..2e17255495 100644 --- a/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy +++ b/modules/nf-lineage/src/test/nextflow/lineage/serde/LinTypeAdapterFactoryTest.groovy @@ -16,7 +16,7 @@ package nextflow.lineage.serde -import nextflow.lineage.model.v1beta1.Checksum + import nextflow.lineage.model.v1beta1.FileOutput import nextflow.lineage.model.v1beta1.LinModel import nextflow.lineage.model.v1beta1.TaskRun @@ -178,4 +178,4 @@ class LinTypeAdapterFactoryTest extends Specification { then: 'should throw exception' thrown(Exception) } -} \ No newline at end of file +}