diff --git a/.changeset/lucky-poems-repeat.md b/.changeset/lucky-poems-repeat.md new file mode 100644 index 00000000000..10954741d95 --- /dev/null +++ b/.changeset/lucky-poems-repeat.md @@ -0,0 +1,5 @@ +--- +'@finos/legend-graph': patch +--- + +Support engine's multi-line (`'''...'''`) string literal protocol changes: `V1_CString` (and its raw value specification counterpart) now carries an optional `multiLine` flag, and a tagged value's `value` accepts both the plain string and the `{ _type: 'string', multiLine: true, value: '...' }` wire shapes. diff --git a/packages/legend-graph/src/graph-manager/__tests__/ImportResolutionRoundtrip.test.ts b/packages/legend-graph/src/graph-manager/__tests__/ImportResolutionRoundtrip.test.ts index d6117003661..006534918e3 100644 --- a/packages/legend-graph/src/graph-manager/__tests__/ImportResolutionRoundtrip.test.ts +++ b/packages/legend-graph/src/graph-manager/__tests__/ImportResolutionRoundtrip.test.ts @@ -29,6 +29,7 @@ import { TEST_DATA__FunctionRoundtrip, TEST_DATA__MeasureRoundtrip, TEST_DATA__ClassWithComplexConstraint, + TEST_DATA__ClassWithMultiLineTaggedValue, TEST_DATA__OverloadedFunctionsRoundtrip, } from './roundtripTestData/TEST_DATA__DomainRoundtrip.js'; import { @@ -123,6 +124,10 @@ describe(unitTest('Domain import resolution roundtrip'), () => { test.each([ ['Class', TEST_DATA__ClassRoundtrip], ['Class with complex constraint', TEST_DATA__ClassWithComplexConstraint], + [ + 'Class with multi-line tagged value', + TEST_DATA__ClassWithMultiLineTaggedValue, + ], ['Enumeration', TEST_DATA__EnumerationRoundtrip], ['Association', TEST_DATA__AssociationRoundtrip], ['Function', TEST_DATA__FunctionRoundtrip], diff --git a/packages/legend-graph/src/graph-manager/__tests__/roundtripTestData/TEST_DATA__DomainRoundtrip.ts b/packages/legend-graph/src/graph-manager/__tests__/roundtripTestData/TEST_DATA__DomainRoundtrip.ts index c1668c19427..3d0270eb0a4 100644 --- a/packages/legend-graph/src/graph-manager/__tests__/roundtripTestData/TEST_DATA__DomainRoundtrip.ts +++ b/packages/legend-graph/src/graph-manager/__tests__/roundtripTestData/TEST_DATA__DomainRoundtrip.ts @@ -1166,3 +1166,52 @@ export const TEST_DATA__OverloadedFunctionsRoundtrip = [ 'meta::pure::metamodel::function::ConcreteFunctionDefinition', }, ]; + +export const TEST_DATA__ClassWithMultiLineTaggedValue = [ + { + path: 'test::A', + content: { + _type: 'class', + name: 'A', + package: 'test', + taggedValues: [ + { + tag: { + profile: 'test::tProf', + value: 'doc', + }, + value: 'a single-line doc', + }, + { + tag: { + profile: 'test::tProf', + value: 'todo', + }, + value: { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + }, + ], + }, + classifierPath: 'meta::pure::metamodel::type::Class', + }, + { + path: 'test::tProf', + content: { + _type: 'profile', + name: 'tProf', + package: 'test', + tags: [ + { + value: 'doc', + }, + { + value: 'todo', + }, + ], + }, + classifierPath: 'meta::pure::metamodel::extension::Profile', + }, +]; diff --git a/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts b/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts index cd0c79009ff..9c546747fbf 100644 --- a/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts +++ b/packages/legend-graph/src/graph-manager/action/changeDetection/DomainObserverHelper.ts @@ -170,6 +170,7 @@ export const observe_TaggedValue = skipObserved( (metamodel: TaggedValue): TaggedValue => { makeObservable(metamodel, { value: observable, + multiLine: observable, hashCode: computed, }); diff --git a/packages/legend-graph/src/graph-manager/action/changeDetection/RawValueSpecificationObserver.ts b/packages/legend-graph/src/graph-manager/action/changeDetection/RawValueSpecificationObserver.ts index b1e5aed9e98..4b5101b4331 100644 --- a/packages/legend-graph/src/graph-manager/action/changeDetection/RawValueSpecificationObserver.ts +++ b/packages/legend-graph/src/graph-manager/action/changeDetection/RawValueSpecificationObserver.ts @@ -33,6 +33,7 @@ export const observe_RawPrimitiveInstanceValue = skipObserved( (metamodel: RawPrimitiveInstanceValue): RawPrimitiveInstanceValue => { makeObservable(metamodel, { value: observable, + multiLine: observable, hashCode: computed, }); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/helpers/V1_ValueSpecificationObserver.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/helpers/V1_ValueSpecificationObserver.ts index ac4aebe5af5..a3461e1c3fc 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/helpers/V1_ValueSpecificationObserver.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/helpers/V1_ValueSpecificationObserver.ts @@ -94,6 +94,8 @@ const V1_observe_PrimitiveType = skipObserved( ) => { V1_observe_Abstract_ValueSpecification(metamodel); + // NOTE: `V1_CString.multiLine` is not observed here as `mobx` throws when annotating a property which does + // not exist on the instance, and this function covers all primitive types. makeObservable(metamodel, { value: observable, }); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/packageableElements/domain/V1_TaggedValue.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/packageableElements/domain/V1_TaggedValue.ts index 3e364ea86a2..8971fc182ae 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/packageableElements/domain/V1_TaggedValue.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/packageableElements/domain/V1_TaggedValue.ts @@ -21,8 +21,21 @@ import type { V1_TagPtr } from '../../../model/packageableElements/domain/V1_Tag export class V1_TaggedValue implements Hashable { tag!: V1_TagPtr; value!: string; + /** + * Whether the value was authored as a multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/5008 + */ + multiLine = false; + /** + * NOTE: must be kept in sync with `TaggedValue.hashCode`. + */ get hashCode(): string { - return hashArray([CORE_HASH_STRUCTURE.TAGGED_VALUE, this.tag, this.value]); + return hashArray([ + CORE_HASH_STRUCTURE.TAGGED_VALUE, + this.tag, + this.value, + this.multiLine.toString(), + ]); } } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/rawValueSpecification/V1_RawPrimitiveInstanceValue.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/rawValueSpecification/V1_RawPrimitiveInstanceValue.ts index 7bc12d641e5..8f6a7f5767f 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/rawValueSpecification/V1_RawPrimitiveInstanceValue.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/rawValueSpecification/V1_RawPrimitiveInstanceValue.ts @@ -29,6 +29,11 @@ export class V1_RawPrimitiveInstanceValue readonly multiplicity = V1_Multiplicity.ONE; type!: string; value?: string | number | boolean | undefined; + /** + * Only meaningful when `type` is `String`: whether the literal was authored as a multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/4998 + */ + multiLine = false; get hashCode(): string { return hashArray([ diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/valueSpecification/raw/V1_CString.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/valueSpecification/raw/V1_CString.ts index 853fc9f8f66..ab613594817 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/valueSpecification/raw/V1_CString.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/model/valueSpecification/raw/V1_CString.ts @@ -28,6 +28,11 @@ export class V1_CString implements Hashable { value!: string; + /** + * Whether the literal was authored as a multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/4998 + */ + multiLine = false; accept_ValueSpecificationVisitor( visitor: V1_ValueSpecificationVisitor, @@ -35,6 +40,10 @@ export class V1_CString return visitor.visit_CString(this); } + /** + * NOTE: `multiLine` is excluded here to keep this hash compatible with `PrimitiveInstanceValue.hashCode`, + * whose metamodel has no counterpart for the flag. + */ override get hashCode(): string { return hashArray([ CORE_HASH_STRUCTURE.PRIMITIVE_INSTANCE_VALUE, diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_DomainTransformer.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_DomainTransformer.ts index 75f1ee9347a..a03d17e2ea1 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_DomainTransformer.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_DomainTransformer.ts @@ -215,6 +215,7 @@ export const V1_transformTaggedValue = ( ): V1_TaggedValue => { const taggedValue = new V1_TaggedValue(); taggedValue.value = element.value; + taggedValue.multiLine = element.multiLine; taggedValue.tag = new V1_TagPtr(); taggedValue.tag.profile = element.tag.ownerReference.valueForSerialization ?? ''; diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_RawValueSpecificationTransformer.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_RawValueSpecificationTransformer.ts index 6a65378a0b4..b25a64d087e 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_RawValueSpecificationTransformer.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/from/V1_RawValueSpecificationTransformer.ts @@ -76,6 +76,7 @@ export class V1_RawValueSpecificationTransformer const protocol = new V1_RawPrimitiveInstanceValue(); protocol.type = rawValueSpecification.type.valueForSerialization ?? ''; protocol.value = rawValueSpecification.value; + protocol.multiLine = rawValueSpecification.multiLine; return protocol; } } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/V1_RawValueSpecificationBuilder.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/V1_RawValueSpecificationBuilder.ts index 944d0c95852..da3aedc4c79 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/V1_RawValueSpecificationBuilder.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/V1_RawValueSpecificationBuilder.ts @@ -57,9 +57,11 @@ export class V1_RawValueSpecificationBuilder visit_PrimitiveInstanceValue( valueSpecification: V1_RawPrimitiveInstanceValue, ): RawValueSpecification { - return new RawPrimitiveInstanceValue( + const metamodel = new RawPrimitiveInstanceValue( this.context.resolveType(valueSpecification.type), valueSpecification.value, ); + metamodel.multiLine = valueSpecification.multiLine; + return metamodel; } } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_DomainBuilderHelper.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_DomainBuilderHelper.ts index e95a4b542f4..87f4bad4894 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_DomainBuilderHelper.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureGraph/to/helpers/V1_DomainBuilderHelper.ts @@ -79,10 +79,12 @@ export const V1_buildTaggedValue = ( context: V1_GraphBuilderContext, ): TaggedValue | undefined => { assertNonNullable(taggedValue.tag, `Tagged value 'tag' field is missing`); - return new TaggedValue( + const metamodel = new TaggedValue( context.resolveTag(taggedValue.tag), taggedValue.value, ); + metamodel.multiLine = taggedValue.multiLine; + return metamodel; }; export const V1_buildConstraint = ( diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_RawValueSpecificationSerialization.test.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_RawValueSpecificationSerialization.test.ts new file mode 100644 index 00000000000..4b0b2795109 --- /dev/null +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_RawValueSpecificationSerialization.test.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2020-present, 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. + */ + +import { test, expect, describe } from '@jest/globals'; +import { guaranteeType, type PlainObject } from '@finos/legend-shared'; +import { unitTest } from '@finos/legend-shared/test'; +import { + V1_deserializeRawValueSpecification, + V1_serializeRawValueSpecification, +} from '../serializationHelpers/V1_RawValueSpecificationSerializationHelper.js'; +import { + TEST__buildGraphWithEntities, + TEST__getTestGraphManagerState, +} from '../../../../../../__test-utils__/GraphManagerTestUtils.js'; +import { RawPrimitiveInstanceValue } from '../../../../../../../graph/metamodel/pure/rawValueSpecification/RawPrimitiveInstanceValue.js'; +import { observe_RawPrimitiveInstanceValue } from '../../../../../../action/changeDetection/RawValueSpecificationObserver.js'; + +type TestCase = [string, PlainObject, PlainObject]; + +const cases: TestCase[] = [ + [ + 'Multi-line raw CString', + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + ], + [ + // engine omits this flag from the wire when it is `false`, we must do the same + 'Single-line raw CString does not emit the multi-line flag', + { + _type: 'string', + multiLine: false, + value: 'hallo', + }, + { + _type: 'string', + value: 'hallo', + }, + ], + [ + 'Legacy format of multi-line raw CString', + { + _type: 'string', + multiLine: true, + values: ['line one\nline two'], + }, + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + ], +]; + +describe(unitTest('Raw value specification serialization'), () => { + test.each(cases)( + '%s', + (testName: TestCase[0], before: TestCase[1], after: TestCase[2]) => { + expect( + V1_serializeRawValueSpecification( + V1_deserializeRawValueSpecification(before), + ), + ).toEqual(after); + // do an additional roundtrip + expect( + V1_serializeRawValueSpecification( + V1_deserializeRawValueSpecification(after), + ), + ).toEqual(after); + }, + ); + + test('Multi-line flag survives the metamodel roundtrip', async () => { + const graphManagerState = TEST__getTestGraphManagerState(); + await TEST__buildGraphWithEntities(graphManagerState, []); + const json = { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }; + + const metamodel = guaranteeType( + graphManagerState.graphManager.buildRawValueSpecification( + json, + graphManagerState.graph, + ), + RawPrimitiveInstanceValue, + ); + expect(metamodel.value).toBe('line one\nline two'); + expect(metamodel.multiLine).toBe(true); + + expect( + graphManagerState.graphManager.serializeRawValueSpecification(metamodel), + ).toEqual(json); + + // the flag must be observable, else change detection would not pick up a toggle + observe_RawPrimitiveInstanceValue(metamodel); + metamodel.multiLine = false; + expect( + graphManagerState.graphManager.serializeRawValueSpecification(metamodel), + ).toEqual({ _type: 'string', value: 'line one\nline two' }); + }); +}); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_TaggedValueBackwardCompatibleSerialization.test.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_TaggedValueBackwardCompatibleSerialization.test.ts new file mode 100644 index 00000000000..d3ebd5a38c2 --- /dev/null +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_TaggedValueBackwardCompatibleSerialization.test.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2020-present, 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. + */ + +import { test, expect, describe } from '@jest/globals'; +import { deserialize, serialize } from 'serializr'; +import { type PlainObject } from '@finos/legend-shared'; +import { unitTest } from '@finos/legend-shared/test'; +import { V1_taggedValueModelSchema } from '../serializationHelpers/V1_CoreSerializationHelper.js'; +import { V1_TaggedValue } from '../../../model/packageableElements/domain/V1_TaggedValue.js'; +import { V1_TagPtr } from '../../../model/packageableElements/domain/V1_TagPtr.js'; +import { V1_transformTaggedValue } from '../../pureGraph/from/V1_DomainTransformer.js'; +import { TaggedValue } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/TaggedValue.js'; +import { Tag } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/Tag.js'; +import { TagExplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/TagReference.js'; +import { Profile } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/Profile.js'; +import { observe_TaggedValue } from '../../../../../../action/changeDetection/DomainObserverHelper.js'; + +const TEST__buildTaggedValue = ( + value: string, + multiLine: boolean, +): V1_TaggedValue => { + const taggedValue = new V1_TaggedValue(); + taggedValue.tag = new V1_TagPtr(); + taggedValue.tag.profile = 'meta::pure::profiles::doc'; + taggedValue.tag.value = 'doc'; + taggedValue.value = value; + taggedValue.multiLine = multiLine; + return taggedValue; +}; + +const TEST__buildTaggedValueJSON = (value: unknown): PlainObject => ({ + tag: { profile: 'meta::pure::profiles::doc', value: 'doc' }, + value, +}); + +describe(unitTest('Tagged value backward-compatible serialization'), () => { + test('Legacy plain string value deserializes', () => { + const taggedValue = deserialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValueJSON('a doc'), + ); + expect(taggedValue.value).toBe('a doc'); + expect(taggedValue.multiLine).toBe(false); + }); + + test('Legacy plain string value with newlines deserializes as single-line', () => { + const taggedValue = deserialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValueJSON('line one\nline two'), + ); + expect(taggedValue.value).toBe('line one\nline two'); + expect(taggedValue.multiLine).toBe(false); + }); + + test('Object value deserializes', () => { + const taggedValue = deserialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValueJSON({ + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }), + ); + expect(taggedValue.value).toBe('line one\nline two'); + expect(taggedValue.multiLine).toBe(true); + }); + + test('Object value without the flag deserializes as single-line', () => { + const taggedValue = deserialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValueJSON({ _type: 'string', value: 'a doc' }), + ); + expect(taggedValue.value).toBe('a doc'); + expect(taggedValue.multiLine).toBe(false); + }); + + test('Single-line value serializes to the legacy shape', () => { + expect( + serialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValue('a doc', false), + ), + ).toEqual(TEST__buildTaggedValueJSON('a doc')); + }); + + test('Multi-line value serializes to an object', () => { + expect( + serialize( + V1_taggedValueModelSchema, + TEST__buildTaggedValue('line one\nline two', true), + ), + ).toEqual( + TEST__buildTaggedValueJSON({ + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }), + ); + }); + + test('Both shapes roundtrip', () => { + [ + TEST__buildTaggedValue('a doc', false), + TEST__buildTaggedValue('line one\nline two', true), + ].forEach((original) => { + const reread = deserialize( + V1_taggedValueModelSchema, + serialize(V1_taggedValueModelSchema, original), + ); + expect(reread.value).toBe(original.value); + expect(reread.multiLine).toBe(original.multiLine); + }); + }); + + test('Toggling the multi-line flag changes the hash', () => { + const profile = new Profile('doc'); + const tag = new Tag(profile, 'doc'); + const taggedValue = observe_TaggedValue( + new TaggedValue(TagExplicitReference.create(tag), 'a doc'), + ); + const hash = taggedValue.hashCode; + + taggedValue.multiLine = true; + + expect(taggedValue.hashCode).not.toBe(hash); + expect(V1_transformTaggedValue(taggedValue).hashCode).toBe( + taggedValue.hashCode, + ); + }); +}); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_ValueSpecificationBackwardCompatibleSerialization.test.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_ValueSpecificationBackwardCompatibleSerialization.test.ts index 046c3ffe56a..22a255b2cbf 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_ValueSpecificationBackwardCompatibleSerialization.test.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/__tests__/V1_ValueSpecificationBackwardCompatibleSerialization.test.ts @@ -44,6 +44,63 @@ const cases: TestCase[] = [ value: 'hallo', }, ], + [ + 'Legacy format of multi-line CString', + { + _type: 'string', + multiLine: true, + multiplicity: { + lowerBound: 1, + upperBound: 1, + }, + values: ['line one\nline two'], + }, + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + ], + [ + 'Multi-line CString', + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + { + _type: 'string', + multiLine: true, + value: 'line one\nline two', + }, + ], + [ + 'Single-line CString does not emit the multi-line flag', + { + _type: 'string', + multiLine: false, + value: 'hallo', + }, + { + _type: 'string', + value: 'hallo', + }, + ], + [ + 'Empty CString with multiplicity of one is converted to an empty string', + { + _type: 'string', + multiplicity: { + lowerBound: 0, + upperBound: 1, + }, + values: [], + }, + { + _type: 'string', + value: '', + }, + ], [ 'Legacy format of CBoolean', { diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_CoreSerializationHelper.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_CoreSerializationHelper.ts index e3cf1043bef..52e02167034 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_CoreSerializationHelper.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_CoreSerializationHelper.ts @@ -14,9 +14,16 @@ * limitations under the License. */ -import { primitive, createModelSchema, optional, deserialize } from 'serializr'; +import { + primitive, + createModelSchema, + custom, + optional, + deserialize, +} from 'serializr'; import { SerializationFactory, + isPlainObject, isString, optionalCustomUsingModelSchema, usingModelSchema, @@ -92,5 +99,23 @@ export const V1_tagPtrModelSchema = createModelSchema(V1_TagPtr, { export const V1_taggedValueModelSchema = createModelSchema(V1_TaggedValue, { tag: usingModelSchema(V1_tagPtrModelSchema), - value: primitive(), + /** + * @backwardCompatibility + * This used to always be a plain JSON string, it is now an object when the value was authored as a + * multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/5008 + */ + value: custom( + (value: string, _key, taggedValue: V1_TaggedValue) => + taggedValue.multiLine + ? { _type: 'string', multiLine: true, value } + : value, + (val, context) => { + if (isPlainObject(val)) { + (context.target as V1_TaggedValue).multiLine = val.multiLine === true; + return val.value ?? ''; + } + return val; + }, + ), }); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_RawValueSpecificationSerializationHelper.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_RawValueSpecificationSerializationHelper.ts index dfc4dc5fd93..d7f43eefea4 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_RawValueSpecificationSerializationHelper.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_RawValueSpecificationSerializationHelper.ts @@ -31,6 +31,7 @@ import { UnsupportedOperationError, usingModelSchema, customList, + optionalCustom, optionalCustomList, optionalCustomListWithSchema, isString, @@ -312,6 +313,11 @@ export const V1_rawPrimitiveInstanceValueSchema = createModelSchema( }, ), ), + // NOTE: like engine, we omit this flag from the wire when it is `false` + multiLine: optionalCustom( + (val) => val, + (val) => val, + ), value: optional(primitive()), }, ); diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_ValueSpecificationSerializer.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_ValueSpecificationSerializer.ts index fd578e2b2a5..1b43ca56a31 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_ValueSpecificationSerializer.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/transformation/pureProtocol/serializationHelpers/V1_ValueSpecificationSerializer.ts @@ -352,6 +352,10 @@ const CIntegerModelSchema = createModelSchema(V1_CInteger, { const CStringModelSchema = createModelSchema(V1_CString, { _type: usingConstantValueSchema(V1_ValueSpecificationType.CSTRING), + multiLine: optionalCustom( + (val) => val, + (val) => val, + ), value: primitive(), }); diff --git a/packages/legend-graph/src/graph/metamodel/pure/packageableElements/domain/TaggedValue.ts b/packages/legend-graph/src/graph/metamodel/pure/packageableElements/domain/TaggedValue.ts index 545b10631dc..5e702b53bb7 100644 --- a/packages/legend-graph/src/graph/metamodel/pure/packageableElements/domain/TaggedValue.ts +++ b/packages/legend-graph/src/graph/metamodel/pure/packageableElements/domain/TaggedValue.ts @@ -23,17 +23,26 @@ export class TaggedValue implements Hashable { tag: TagReference; value: string; + /** + * Whether the value was authored as a multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/5008 + */ + multiLine = false; constructor(tag: TagReference, value: string) { this.tag = tag; this.value = value; } + /** + * NOTE: `V1_TaggedValue.hashCode` must be kept in sync. + */ get hashCode(): string { return hashArray([ CORE_HASH_STRUCTURE.TAGGED_VALUE, this.tag.pointerHashCode, this.value, + this.multiLine.toString(), ]); } } diff --git a/packages/legend-graph/src/graph/metamodel/pure/rawValueSpecification/RawPrimitiveInstanceValue.ts b/packages/legend-graph/src/graph/metamodel/pure/rawValueSpecification/RawPrimitiveInstanceValue.ts index 11c231e1273..85e91e1bf45 100644 --- a/packages/legend-graph/src/graph/metamodel/pure/rawValueSpecification/RawPrimitiveInstanceValue.ts +++ b/packages/legend-graph/src/graph/metamodel/pure/rawValueSpecification/RawPrimitiveInstanceValue.ts @@ -31,6 +31,11 @@ export class RawPrimitiveInstanceValue readonly multiplicity = Multiplicity.ONE; type: PackageableElementReference; value?: string | number | boolean | undefined; + /** + * Only meaningful when `type` is `String`: whether the literal was authored as a multi-line (`'''...'''`) block. + * See https://github.com/finos/legend-engine/pull/4998 + */ + multiLine = false; constructor( type: PackageableElementReference,