From 9e9d3f4b4668592af6513ca440a0afc7a89b5d3d Mon Sep 17 00:00:00 2001 From: Mateusz Klatt Date: Tue, 10 Oct 2023 14:44:55 +0200 Subject: [PATCH] implement RenamedClass token functionality (#2351) --- legend-engine-xts-changetoken/README.md | 1 + .../GenerateCastRenamedClassTest.java | 180 ++++++++++++++++++ .../cast_generation.pure | 27 +++ .../core_pure_changetoken/changetoken.pure | 12 ++ 4 files changed, 220 insertions(+) create mode 100644 legend-engine-xts-changetoken/legend-engine-xt-changetoken-compiler/src/test/java/org/finos/legend/engine/changetoken/generation/GenerateCastRenamedClassTest.java diff --git a/legend-engine-xts-changetoken/README.md b/legend-engine-xts-changetoken/README.md index 2e11c1abb55..d01d986a285 100644 --- a/legend-engine-xts-changetoken/README.md +++ b/legend-engine-xts-changetoken/README.md @@ -15,6 +15,7 @@ For those kind of detected operations user confirmation should be required, in o ## Change Token Types - **AddedClass** / **RemovedClass** - current no-op - should be emitted if a class of entity was added/removed. +- **RenamedClass** - token describing that a class was renamed or moved to another package. - **AddField** / **RemoveField** - token describing that a field was added or removed to specific entity class. This token should provide **fieldName**, **fieldType** and **class** properties together with **defaultValue**, of e.g. **ConstValue** **@type**. The **value** should be of expected primitive or nested object type (in case of object type **@type** property is mandatory). diff --git a/legend-engine-xts-changetoken/legend-engine-xt-changetoken-compiler/src/test/java/org/finos/legend/engine/changetoken/generation/GenerateCastRenamedClassTest.java b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-compiler/src/test/java/org/finos/legend/engine/changetoken/generation/GenerateCastRenamedClassTest.java new file mode 100644 index 00000000000..8ee7752ff70 --- /dev/null +++ b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-compiler/src/test/java/org/finos/legend/engine/changetoken/generation/GenerateCastRenamedClassTest.java @@ -0,0 +1,180 @@ +// Copyright 2023 Goldman Sachs +// +// 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 org.finos.legend.engine.changetoken.generation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +import static org.junit.Assert.assertThrows; + +public class GenerateCastRenamedClassTest extends GenerateCastTestBase +{ + @BeforeClass + public static void setupSuite() throws IOException, ClassNotFoundException + { + setupSuiteFromJson("{\n" + + " \"@type\": \"meta::pure::changetoken::Versions\",\n" + + " \"versions\": [\n" + + " {\n" + + " \"@type\": \"meta::pure::changetoken::Version\",\n" + + " \"version\": \"ftdm:abcdefg123\"\n" + + " },\n" + + " {\n" + + " \"@type\": \"meta::pure::changetoken::Version\",\n" + + " \"version\": \"ftdm:abcdefg456\",\n" + + " \"prevVersion\": \"ftdm:abcdefg123\",\n" + + " \"changeTokens\": [\n" + + " {\n" + + " \"@type\": \"meta::pure::changetoken::RenamedClass\",\n" + + " \"newName\": \"meta::pure::changetoken::tests::nested::NewSampleClass\",\n" + + " \"class\": \"meta::pure::changetoken::tests::SampleClass\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}\n"); + } + + @Test + public void testUpcast() throws JsonProcessingException, NoSuchMethodException, InvocationTargetException, IllegalAccessException + { + Map jsonNode = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg123\", \n" + + " \"@type\": \"meta::pure::changetoken::tests::SampleClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"1d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"2d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"3d\"}}]\n" + + " ],\n" + + " \"abc\": {\"@type\":\"Custom\", \"value\":\"4d\"}\n" + + "}", Map.class); + Map jsonNodeOut = (Map) compiledClass.getMethod("upcast", Map.class).invoke(null, jsonNode); + + Map expectedJsonNodeOut = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg456\",\n" + + " \"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"1d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"2d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"3d\"}}]\n" + + " ],\n" + + " \"abc\": {\"@type\":\"Custom\", \"value\":\"4d\"}\n" + + "}", Map.class); // updated version and new default value field added + Assert.assertEquals(expectedJsonNodeOut, jsonNodeOut); + } + + @Test + public void testUpcastType() throws JsonProcessingException, NoSuchMethodException, InvocationTargetException, IllegalAccessException + { + Map jsonNode = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg123\", \n" + + " \"@type\": \"meta::pure::changetoken::tests::OtherClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"1d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"2d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"3d\"}}]\n" + + " ],\n" + + " \"abc\": {\"@type\":\"Custom\", \"value\":\"4d\"}\n" + + "}", Map.class); + Map jsonNodeOut = (Map) compiledClass.getMethod("upcast", Map.class).invoke(null, jsonNode); + + Map expectedJsonNodeOut = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg456\",\n" + + " \"@type\": \"meta::pure::changetoken::tests::OtherClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"1d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"2d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"abc\": {\"@type\":\"Custom\", \"value\":\"3d\"}}]\n" + + " ],\n" + + " \"abc\": {\"@type\":\"Custom\", \"value\":\"4d\"}\n" + + "}", Map.class); // updated version and new default value field added + Assert.assertEquals(expectedJsonNodeOut, jsonNodeOut); + } + + @Test + public void testDowncast() throws JsonProcessingException, NoSuchMethodException, InvocationTargetException, IllegalAccessException + { + ObjectMapper mapper = new ObjectMapper(); + Map jsonNode = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg456\",\n" + + " \"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"4d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"3d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::nested::NewSampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"2d\"}}]\n" + + " ],\n" + + " \"xyz\": {\"@type\":\"Custom\", \"value\":\"1d\"}\n" + + "}", Map.class); + Map jsonNodeOut = (Map) compiledClass.getMethod("downcast", Map.class, String.class) + .invoke(null, jsonNode, "ftdm:abcdefg123"); + Map expectedJsonNodeOut = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg123\", \n" + + " \"@type\": \"meta::pure::changetoken::tests::SampleClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"4d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"3d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::SampleClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"2d\"}}]\n" + + " ],\n" + + " \"xyz\": {\"@type\":\"Custom\", \"value\":\"1d\"}\n" + + "}", Map.class); // remove default values + Assert.assertEquals(expectedJsonNodeOut, jsonNodeOut); + } + + @Test + public void testDowncastType() throws JsonProcessingException, NoSuchMethodException, InvocationTargetException, IllegalAccessException + { + ObjectMapper mapper = new ObjectMapper(); + Map jsonNode = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg456\",\n" + + " \"@type\": \"meta::pure::changetoken::tests::OtherClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"4d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"3d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"2d\"}}]\n" + + " ],\n" + + " \"xyz\": {\"@type\":\"Custom\", \"value\":\"1d\"}\n" + + "}", Map.class); + Map jsonNodeOut = (Map) compiledClass.getMethod("downcast", Map.class, String.class) + .invoke(null, jsonNode, "ftdm:abcdefg123"); + Map expectedJsonNodeOut = mapper.readValue( + "{\n" + + " \"version\":\"ftdm:abcdefg123\", \n" + + " \"@type\": \"meta::pure::changetoken::tests::OtherClass\",\n" + + " \"innerObject\": {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"4d\"}},\n" + + " \"innerNestedArray\":[\n" + + " {\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"3d\"}},\n" + + " [{\"@type\": \"meta::pure::changetoken::tests::OtherClass\", \"xyz\": {\"@type\":\"Custom\", \"value\":\"2d\"}}]\n" + + " ],\n" + + " \"xyz\": {\"@type\":\"Custom\", \"value\":\"1d\"}\n" + + "}", Map.class); // remove default values + Assert.assertEquals(expectedJsonNodeOut, jsonNodeOut); + } +} + diff --git a/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/cast_generation.pure b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/cast_generation.pure index e777e1b268b..bf0afebd2b8 100644 --- a/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/cast_generation.pure +++ b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/cast_generation.pure @@ -268,6 +268,7 @@ function <> meta::pure::changetoken::cast_generation::_generateS t:RenameField[1]|_handleRenameFieldUpcast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar), t:AddedClass[1]|[], t:RemovedClass[1]|[], + t:RenamedClass[1]|_handleRenamedClassUpcast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar), t:ChangeFieldType[1]|_handleChangeFieldTypeUpcast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar) ])) ->fold({t,res|$res->concatenate($t)}, []); @@ -343,6 +344,7 @@ function <> meta::pure::changetoken::cast_generation::_generateS t:RenameField[1]|_handleRenameFieldDowncast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar), t:AddedClass[1]|[], t:RemovedClass[1]|[], + t:RenamedClass[1]|_handleRenamedClassDowncast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar), t:ChangeFieldType[1]|_handleChangeFieldTypeDowncast($class2, $t, $objVar, $typeVar, $version.version, $pathVar, $rootObjVar, $resVar) ])) ->fold({t,res|$res->concatenate($t)}, []); @@ -569,6 +571,31 @@ function <> meta::pure::changetoken::cast_generation::_handleRen ); } +function <> meta::pure::changetoken::cast_generation::_handleRenamedClassUpcast +( +class:meta::external::language::java::metamodel::Class[1], +token:RenamedClass[1], objVar:Code[1], typeVar:Code[1], version:String[1], pathVar:Code[1], rootObjVar:Code[1], resVar:Code[1] +):Code[1] +{ + // if (type.equals(token.class)) + j_if( + $typeVar->j_invoke('equals', [j_string($token.class)]), + $objVar->j_invoke('put', [j_string('@type'), j_string($token.newName)]) + ); +} + +function <> meta::pure::changetoken::cast_generation::_handleRenamedClassDowncast +( +class:meta::external::language::java::metamodel::Class[1], +token:RenamedClass[1], objVar:Code[1], typeVar:Code[1], version:String[1], pathVar:Code[1], rootObjVar:Code[1], resVar:Code[1] +):Code[1] +{ + // if (type.equals(token.newName)) + j_if( + $typeVar->j_invoke('equals', [j_string($token.newName)]), + $objVar->j_invoke('put', [j_string('@type'), j_string($token.class)]) + ); +} function <> meta::pure::changetoken::cast_generation::_handleAddFieldUpcast ( diff --git a/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/changetoken.pure b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/changetoken.pure index bbeecfe3b36..4872e4904a9 100644 --- a/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/changetoken.pure +++ b/legend-engine-xts-changetoken/legend-engine-xt-changetoken-pure/src/main/resources/core_pure_changetoken/changetoken.pure @@ -85,6 +85,18 @@ meta::pure::changetoken::RemovedClass extends ClassChangeToken + ')' }: String[1]; } +Class +{doc.doc='a class was renamed in this version'} +meta::pure::changetoken::RenamedClass extends ClassChangeToken +{ + newName: String[1]; + + toString() { 'RenamedClass(' + + 'class=' + $this.class->toString() + + 'newName=' + $this.newName->toString() + + ')' }: String[1]; +} + Class <> {doc.doc='root of a hierarchy of getters for a default value of a newly-added field'} meta::pure::changetoken::FieldGetter