diff --git a/packages/ui/src/components/InlineEdit/routeIdValidator.test.ts b/packages/ui/src/components/InlineEdit/routeIdValidator.test.ts index a7468b280..1dfad04d5 100644 --- a/packages/ui/src/components/InlineEdit/routeIdValidator.test.ts +++ b/packages/ui/src/components/InlineEdit/routeIdValidator.test.ts @@ -18,7 +18,7 @@ describe('routeIdValidator', () => { }); it('should return sucess if the name is unique', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); const visualEntities = resource.getVisualEntities(); jest.spyOn(visualEntities[0], 'getId').mockReturnValue('flow-1234'); @@ -28,7 +28,7 @@ describe('routeIdValidator', () => { }); it('should return an error if the name is not unique', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); const visualEntities = resource.getVisualEntities(); jest.spyOn(visualEntities[0], 'getId').mockReturnValue('flow-1234'); @@ -39,7 +39,7 @@ describe('routeIdValidator', () => { }); it('should return an error if the name is not a valid URI', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); const visualEntities = resource.getVisualEntities(); jest.spyOn(visualEntities[0], 'getId').mockReturnValue('flow-1234'); @@ -50,7 +50,7 @@ describe('routeIdValidator', () => { }); it('should return an error if the name is not unique neither a valid URI', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); const visualEntities = resource.getVisualEntities(); jest.spyOn(visualEntities[0], 'getId').mockReturnValue('The amazing Route'); diff --git a/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx b/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx index 2262a8223..6801b3638 100644 --- a/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx +++ b/packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx @@ -77,7 +77,7 @@ describe('Canvas', () => { }); it('should be able to delete the routes', async () => { - const camelResource = new CamelRouteResource(camelRouteJson); + const camelResource = new CamelRouteResource([camelRouteJson]); const routeEntities = camelResource.getVisualEntities(); const removeSpy = jest.spyOn(camelResource, 'removeEntity'); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemEnableAllSteps.test.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemEnableAllSteps.test.tsx index db58f5b07..798f18cc9 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemEnableAllSteps.test.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemEnableAllSteps.test.tsx @@ -20,7 +20,7 @@ describe('ItemEnableAllSteps', () => { }); it('should NOT render an ItemEnableAllSteps if there are not at least 2 or more disabled steps', () => { - const camelResource = new CamelRouteResource(camelRouteJson); + const camelResource = new CamelRouteResource([camelRouteJson]); const visualEntity = camelResource.getVisualEntities()[0]; const { nodes, edges } = FlowService.getFlowDiagram(visualEntity.toVizNode()); @@ -50,7 +50,7 @@ describe('ItemEnableAllSteps', () => { }); it('should call updateModel and updateEntitiesFromCamelResource on click', async () => { - const camelResource = new CamelRouteResource(camelRouteWithDisabledSteps); + const camelResource = new CamelRouteResource([camelRouteWithDisabledSteps]); const visualEntity = camelResource.getVisualEntities()[0]; const { nodes, edges } = FlowService.getFlowDiagram(visualEntity.toVizNode()); diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.test.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.test.tsx index a1a979037..43b83c620 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.test.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/NodeContextMenu.test.tsx @@ -107,7 +107,7 @@ describe('NodeContextMenu', () => { }); it('should render an ItemEnableAllSteps', () => { - const camelResource = new CamelRouteResource(camelRouteWithDisabledSteps); + const camelResource = new CamelRouteResource([camelRouteWithDisabledSteps]); const visualEntity = camelResource.getVisualEntities()[0]; const { nodes, edges } = FlowService.getFlowDiagram(visualEntity.toVizNode()); diff --git a/packages/ui/src/hooks/entities.test.tsx b/packages/ui/src/hooks/entities.test.tsx index 249767606..1f9c5bc7d 100644 --- a/packages/ui/src/hooks/entities.test.tsx +++ b/packages/ui/src/hooks/entities.test.tsx @@ -59,7 +59,7 @@ describe('useEntities', () => { expect(notifierSpy).toHaveBeenCalledWith('entities:updated', camelRouteYaml_1_1_updated); }); - it('should notifiy subscribers when the entities are updated', () => { + it('should notify subscribers when the entities are updated', () => { const notifierSpy = jest.spyOn(eventNotifier, 'next'); const { result } = renderHook(() => useEntities()); @@ -162,10 +162,8 @@ describe('useEntities', () => { `, ); }); - - describe('comments', () => { - it(`should store code's comments`, () => { - const code = `# This is a comment + it(`should store code's comments`, () => { + const code = `# This is a comment # An indented comment - route: @@ -183,48 +181,15 @@ describe('useEntities', () => { message: \${body} `; - const { result } = renderHook(() => useEntities()); - - act(() => { - eventNotifier.next('code:updated', code); - }); + const { result } = renderHook(() => useEntities()); - expect(result.current.camelResource.getComments()).toEqual([ - '# This is a comment', - ' # An indented comment', - '', - ]); + act(() => { + eventNotifier.next('code:updated', code); }); - it('should add comments to the source code', () => { - const notifierSpy = jest.spyOn(eventNotifier, 'next'); - const { result } = renderHook(() => useEntities()); - - act(() => { - result.current.camelResource.setComments(['# This is a comment', ' # An indented comment', '']); - result.current.camelResource.addNewEntity(); - result.current.updateSourceCodeFromEntities(); - }); - - expect(notifierSpy).toHaveBeenCalledWith( - 'entities:updated', - `# This is a comment - # An indented comment - -- route: - id: route-1234 - from: - id: from-1234 - uri: timer - parameters: - period: "1000" - timerName: template - steps: - - log: - id: log-1234 - message: \${body} -`, - ); - }); + expect(result.current.camelResource.toString()).toContain( + `# This is a comment + # An indented comment`, + ); }); }); diff --git a/packages/ui/src/hooks/entities.ts b/packages/ui/src/hooks/entities.ts index f376a404d..b0a7b03ae 100644 --- a/packages/ui/src/hooks/entities.ts +++ b/packages/ui/src/hooks/entities.ts @@ -1,21 +1,10 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; -import { parse, stringify } from 'yaml'; -import { CamelResource, SourceSchemaType, createCamelResource } from '../models/camel'; + +import { CamelResource, SourceSchemaType } from '../models/camel'; import { BaseCamelEntity } from '../models/camel/entities'; import { BaseVisualCamelEntity } from '../models/visualization/base-visual-entity'; import { EventNotifier } from '../utils'; - -/** - * Regular expression to match commented lines, regardless of indentation - * Given the following examples, the regular expression should match the comments: - * ``` - * # This is a comment - * # This is an indented comment - *# This is an indented comment - * ``` - * The regular expression should match the first three lines - */ -const COMMENTED_LINES_REGEXP = /^\s*#.*$/; +import { CamelResourceFactory } from '../models/camel/camel-resource-factory'; export interface EntitiesContextResult { entities: BaseCamelEntity[]; @@ -49,7 +38,7 @@ export interface EntitiesContextResult { export const useEntities = (): EntitiesContextResult => { const eventNotifier = EventNotifier.getInstance(); - const [camelResource, setCamelResource] = useState(createCamelResource()); + const [camelResource, setCamelResource] = useState(CamelResourceFactory.createCamelResource()); const [entities, setEntities] = useState([]); const [visualEntities, setVisualEntities] = useState([]); @@ -58,22 +47,10 @@ export const useEntities = (): EntitiesContextResult => { */ useLayoutEffect(() => { return eventNotifier.subscribe('code:updated', (code) => { - /** Extract comments from the source code */ - const lines = code.split('\n'); - const comments: string[] = []; - for (const line of lines) { - if (line.trim() === '' || COMMENTED_LINES_REGEXP.test(line)) { - comments.push(line); - } else { - break; - } - } - - const rawEntities = parse(code); - const camelResource = createCamelResource(rawEntities); - camelResource.setComments(comments); + const camelResource = CamelResourceFactory.createCamelResource(code); const entities = camelResource.getEntities(); const visualEntities = camelResource.getVisualEntities(); + setCamelResource(camelResource); setEntities(entities); setVisualEntities(visualEntities); @@ -81,13 +58,7 @@ export const useEntities = (): EntitiesContextResult => { }, [eventNotifier]); const updateSourceCodeFromEntities = useCallback(() => { - let code = stringify(camelResource, { sortMapEntries: camelResource.sortFn, schema: 'yaml-1.1' }) || ''; - - if (camelResource.getComments().length > 0) { - const comments = camelResource.getComments().join('\n'); - code = comments + '\n' + code; - } - + const code = camelResource.toString(); eventNotifier.next('entities:updated', code); }, [camelResource, eventNotifier]); @@ -105,7 +76,7 @@ export const useEntities = (): EntitiesContextResult => { const setCurrentSchemaType = useCallback( (type: SourceSchemaType) => { - setCamelResource(createCamelResource(type)); + setCamelResource(CamelResourceFactory.createCamelResource(type)); updateEntitiesFromCamelResource(); }, [updateEntitiesFromCamelResource], diff --git a/packages/ui/src/models/camel/__snapshots__/camel-resource.test.ts.snap b/packages/ui/src/models/camel/__snapshots__/camel-resource.test.ts.snap index 27a2e2020..287d4771a 100644 --- a/packages/ui/src/models/camel/__snapshots__/camel-resource.test.ts.snap +++ b/packages/ui/src/models/camel/__snapshots__/camel-resource.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`createCamelResource should create an empty KameletResource if no args is specified 1`] = ` +exports[`CamelResourceFactory.createCamelResource should create an empty KameletResource if no args is specified 1`] = ` [ { "from": { diff --git a/packages/ui/src/models/camel/camel-k-resource-factory.ts b/packages/ui/src/models/camel/camel-k-resource-factory.ts new file mode 100644 index 000000000..bd5cdd1ef --- /dev/null +++ b/packages/ui/src/models/camel/camel-k-resource-factory.ts @@ -0,0 +1,35 @@ +import { SourceSchemaType } from './source-schema-type'; +import { CamelResource } from './camel-resource'; +import { IntegrationResource } from './integration-resource'; +import { KameletResource } from './kamelet-resource'; +import { KameletBindingResource } from './kamelet-binding-resource'; +import { PipeResource } from './pipe-resource'; +import { IKameletDefinition } from '../kamelets-catalog'; +import { + Integration as IntegrationType, + KameletBinding as KameletBindingType, + Pipe as PipeType, +} from '@kaoto/camel-catalog/types'; + +export class CamelKResourceFactory { + static getCamelKResource( + json?: IntegrationType | IKameletDefinition | KameletBindingType | PipeType, + type?: SourceSchemaType, + ): CamelResource | undefined { + const jsonRecord = json ? (json as Record) : {}; + + if ((jsonRecord && typeof json === 'object' && 'kind' in jsonRecord) || type) { + switch (jsonRecord['kind'] || type) { + case SourceSchemaType.Integration: + return new IntegrationResource(json as IntegrationType); + case SourceSchemaType.Kamelet: + return new KameletResource(json as IKameletDefinition); + case SourceSchemaType.KameletBinding: + return new KameletBindingResource(json as KameletBindingType); + case SourceSchemaType.Pipe: + return new PipeResource(json as PipeType); + } + } + return undefined; + } +} diff --git a/packages/ui/src/models/camel/camel-k-resource.ts b/packages/ui/src/models/camel/camel-k-resource.ts index c13bc9362..6b6ddb32d 100644 --- a/packages/ui/src/models/camel/camel-k-resource.ts +++ b/packages/ui/src/models/camel/camel-k-resource.ts @@ -11,6 +11,7 @@ import { MetadataEntity } from '../visualization/metadata'; import { BaseVisualCamelEntityDefinition, CamelResource } from './camel-resource'; import { BaseCamelEntity } from './entities'; import { SourceSchemaType } from './source-schema-type'; +import { CamelResourceSerializer, YamlCamelResourceSerializer } from '../../serializers'; export type CamelKType = IntegrationType | IKameletDefinition | KameletBindingType | PipeType; @@ -28,11 +29,13 @@ export abstract class CamelKResource implements CamelResource { readonly sortFn = createCamelPropertiesSorter(CamelKResource.PARAMETERS_ORDER) as (a: unknown, b: unknown) => number; protected resource: CamelKType; private metadata?: MetadataEntity; - private comments: string[] = []; - constructor(resource?: CamelKType) { - if (resource) { - this.resource = resource; + constructor( + parsedResource: unknown, + private readonly serializer: CamelResourceSerializer = new YamlCamelResourceSerializer(), + ) { + if (parsedResource) { + this.resource = parsedResource as CamelKType; } else { this.resource = { apiVersion: CAMEL_K_K8S_API_VERSION_V1, @@ -94,12 +97,15 @@ export abstract class CamelKResource implements CamelResource { getCompatibleComponents(_mode: AddStepMode, _visualEntityData: IVisualizationNodeData): TileFilter | undefined { return undefined; } + getSerializer() { + return this.serializer; + } - setComments(comments: string[]) { - this.comments = comments; + setSerializer(_serializer: CamelResourceSerializer): void { + /** Not supported by default */ } - getComments(): string[] { - return this.comments; + toString(): string { + return this.serializer.serialize(this); } } diff --git a/packages/ui/src/models/camel/camel-resource-factory.ts b/packages/ui/src/models/camel/camel-resource-factory.ts new file mode 100644 index 000000000..83a4abc6c --- /dev/null +++ b/packages/ui/src/models/camel/camel-resource-factory.ts @@ -0,0 +1,32 @@ +import { SourceSchemaType } from './source-schema-type'; +import { CamelResource } from './camel-resource'; +import { CamelResourceSerializer, XmlCamelResourceSerializer, YamlCamelResourceSerializer } from '../../serializers'; +import { CamelRouteResource } from './camel-route-resource'; +import { CamelKResourceFactory } from './camel-k-resource-factory'; +import { CamelYamlDsl, Integration, KameletBinding, Pipe } from '@kaoto/camel-catalog/types'; +import { IKameletDefinition } from '../kamelets-catalog'; + +export class CamelResourceFactory { + /** + * Creates a CamelResource based on the given {@link type} and {@link source}. If + * both are not specified, a default empty {@link CamelRouteResource} is created. + * If only {@link type} is specified, an empty {@link CamelResource} of the given + * {@link type} is created. + * @param type + * @param source + */ + static createCamelResource(source?: string, type?: SourceSchemaType): CamelResource { + const serializer: CamelResourceSerializer = XmlCamelResourceSerializer.isApplicable(source) + ? new XmlCamelResourceSerializer() + : new YamlCamelResourceSerializer(); + + const parsedCode = typeof source === 'string' ? serializer.parse(source) : source; + const resource = CamelKResourceFactory.getCamelKResource( + parsedCode as Integration | KameletBinding | Pipe | IKameletDefinition, + type, + ); + + if (resource) return resource; + return new CamelRouteResource(parsedCode as CamelYamlDsl, serializer); + } +} diff --git a/packages/ui/src/models/camel/camel-resource.test.ts b/packages/ui/src/models/camel/camel-resource.test.ts index 99af20671..a2a55b3fc 100644 --- a/packages/ui/src/models/camel/camel-resource.test.ts +++ b/packages/ui/src/models/camel/camel-resource.test.ts @@ -1,50 +1,50 @@ -import { camelRouteJson } from '../../stubs/camel-route'; +import { camelRouteYaml } from '../../stubs/camel-route'; import { integrationJson } from '../../stubs/integration'; import { kameletBindingJson } from '../../stubs/kamelet-binding-route'; import { kameletJson } from '../../stubs/kamelet-route'; import { pipeJson } from '../../stubs/pipe'; import { CamelRouteVisualEntity, PipeVisualEntity } from '../visualization/flows'; -import { createCamelResource } from './camel-resource'; import { SourceSchemaType } from './source-schema-type'; +import { CamelResourceFactory } from './camel-resource-factory'; -describe('createCamelResource', () => { +describe('CamelResourceFactory.createCamelResource', () => { it('should create an empty CamelRouteResource if no args is specified', () => { - const resource = createCamelResource(); + const resource = CamelResourceFactory.createCamelResource(); expect(resource.getType()).toEqual(SourceSchemaType.Route); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities()).toEqual([]); }); it('should create an empty CamelRouteResource if no args is specified', () => { - const resource = createCamelResource(undefined, SourceSchemaType.Route); + const resource = CamelResourceFactory.createCamelResource(undefined, SourceSchemaType.Route); expect(resource.getType()).toEqual(SourceSchemaType.Route); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities()).toEqual([]); }); it('should create an empty IntegrationResource if no args is specified', () => { - const resource = createCamelResource(undefined, SourceSchemaType.Integration); + const resource = CamelResourceFactory.createCamelResource(undefined, SourceSchemaType.Integration); expect(resource.getType()).toEqual(SourceSchemaType.Integration); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities()).toEqual([]); }); it('should create an empty KameletResource if no args is specified', () => { - const resource = createCamelResource(undefined, SourceSchemaType.Kamelet); + const resource = CamelResourceFactory.createCamelResource(undefined, SourceSchemaType.Kamelet); expect(resource.getType()).toEqual(SourceSchemaType.Kamelet); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities()).toMatchSnapshot(); }); it('should create an empty CameletBindingResource if no args is specified', () => { - const resource = createCamelResource(undefined, SourceSchemaType.KameletBinding); + const resource = CamelResourceFactory.createCamelResource(undefined, SourceSchemaType.KameletBinding); expect(resource.getType()).toEqual(SourceSchemaType.KameletBinding); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities().length).toEqual(1); }); it('should create an empty PipeResource if no args is specified', () => { - const resource = createCamelResource(undefined, SourceSchemaType.Pipe); + const resource = CamelResourceFactory.createCamelResource(undefined, SourceSchemaType.Pipe); expect(resource.getType()).toEqual(SourceSchemaType.Pipe); expect(resource.getEntities()).toEqual([]); expect(resource.getVisualEntities().length).toEqual(1); @@ -55,7 +55,7 @@ describe('createCamelResource', () => { }); it('should create a camel route', () => { - const resource = createCamelResource(camelRouteJson); + const resource = CamelResourceFactory.createCamelResource(camelRouteYaml); expect(resource.getType()).toEqual(SourceSchemaType.Route); expect(resource.getVisualEntities().length).toEqual(1); const vis = resource.getVisualEntities()[0] as CamelRouteVisualEntity; @@ -64,19 +64,19 @@ describe('createCamelResource', () => { // TODO it.skip('should create an Integration', () => { - const resource = createCamelResource(integrationJson); + const resource = CamelResourceFactory.createCamelResource(JSON.stringify(integrationJson)); expect(resource.getType()).toEqual(SourceSchemaType.Integration); expect(resource.getVisualEntities().length).toEqual(2); }); it('should create a Kamelet', () => { - const resource = createCamelResource(kameletJson); + const resource = CamelResourceFactory.createCamelResource(JSON.stringify(kameletJson)); expect(resource.getType()).toEqual(SourceSchemaType.Kamelet); expect(resource.getVisualEntities().length).toEqual(1); }); it('should create a KameletBindingPipe', () => { - const resource = createCamelResource(kameletBindingJson); + const resource = CamelResourceFactory.createCamelResource(JSON.stringify(kameletBindingJson)); expect(resource.getType()).toEqual(SourceSchemaType.KameletBinding); expect(resource.getVisualEntities().length).toEqual(1); const vis = resource.getVisualEntities()[0] as PipeVisualEntity; @@ -84,7 +84,7 @@ describe('createCamelResource', () => { }); it('should create a Pipe', () => { - const resource = createCamelResource(pipeJson); + const resource = CamelResourceFactory.createCamelResource(JSON.stringify(pipeJson)); expect(resource.getType()).toEqual(SourceSchemaType.Pipe); expect(resource.getVisualEntities().length).toEqual(1); const vis = resource.getVisualEntities()[0] as PipeVisualEntity; diff --git a/packages/ui/src/models/camel/camel-resource.ts b/packages/ui/src/models/camel/camel-resource.ts index 965cd0fc6..79c0d073d 100644 --- a/packages/ui/src/models/camel/camel-resource.ts +++ b/packages/ui/src/models/camel/camel-resource.ts @@ -1,20 +1,10 @@ -import { - Integration as IntegrationType, - KameletBinding as KameletBindingType, - Pipe as PipeType, -} from '@kaoto/camel-catalog/types'; import { TileFilter } from '../../components/Catalog'; -import { IKameletDefinition } from '../kamelets-catalog'; import { AddStepMode, BaseVisualCamelEntity, IVisualizationNodeData } from '../visualization/base-visual-entity'; import { BeansEntity } from '../visualization/metadata'; import { RouteTemplateBeansEntity } from '../visualization/metadata/routeTemplateBeansEntity'; -import { CamelRouteResource } from './camel-route-resource'; import { BaseCamelEntity, EntityType } from './entities'; -import { IntegrationResource } from './integration-resource'; -import { KameletBindingResource } from './kamelet-binding-resource'; -import { KameletResource } from './kamelet-resource'; -import { PipeResource } from './pipe-resource'; import { SourceSchemaType } from './source-schema-type'; +import { CamelResourceSerializer } from '../../serializers'; export interface CamelResource { getVisualEntities(): BaseVisualCamelEntity[]; @@ -23,8 +13,11 @@ export interface CamelResource { removeEntity(id?: string): void; supportsMultipleVisualEntities(): boolean; toJSON(): unknown; + toString(): string; getType(): SourceSchemaType; getCanvasEntityList(): BaseVisualCamelEntityDefinition; + getSerializer(): CamelResourceSerializer; + setSerializer(serializer: CamelResourceSerializer): void; /** Components Catalog related methods */ getCompatibleComponents( @@ -35,8 +28,6 @@ export interface CamelResource { ): TileFilter | undefined; sortFn?: (a: unknown, b: unknown) => number; - setComments(comments: string[]): void; - getComments(): string[]; } export interface BaseVisualCamelEntityDefinition { @@ -60,35 +51,3 @@ export interface RouteTemplateBeansAwareResource { getRouteTemplateBeansEntity(): RouteTemplateBeansEntity | undefined; deleteRouteTemplateBeansEntity(): void; } - -/** - * Creates a CamelResource based on the given {@link type} and {@link json}. If - * both are not specified, a default empty {@link CamelRouteResource} is created. - * If only {@link type} is specified, an empty {@link CamelResource} of the given - * {@link type} is created. - * @param type - * @param json - */ -export function createCamelResource(json?: unknown, type?: SourceSchemaType): CamelResource { - const jsonRecord = json as Record; - if (json && typeof json === 'object' && 'kind' in jsonRecord) { - return doCreateCamelResource(json, jsonRecord['kind'] as SourceSchemaType); - } else { - return doCreateCamelResource(json, type || SourceSchemaType.Route); - } -} - -function doCreateCamelResource(json?: unknown, type?: SourceSchemaType): CamelResource { - switch (type) { - case SourceSchemaType.Integration: - return new IntegrationResource(json as IntegrationType); - case SourceSchemaType.Kamelet: - return new KameletResource(json as IKameletDefinition); - case SourceSchemaType.KameletBinding: - return new KameletBindingResource(json as KameletBindingType); - case SourceSchemaType.Pipe: - return new PipeResource(json as PipeType); - default: - return new CamelRouteResource(json); - } -} diff --git a/packages/ui/src/models/camel/camel-route-resource.test.ts b/packages/ui/src/models/camel/camel-route-resource.test.ts index 2a013d0f5..1c29e4a34 100644 --- a/packages/ui/src/models/camel/camel-route-resource.test.ts +++ b/packages/ui/src/models/camel/camel-route-resource.test.ts @@ -1,19 +1,20 @@ import { beansJson } from '../../stubs/beans'; import { camelFromJson } from '../../stubs/camel-from'; -import { camelRouteJson } from '../../stubs/camel-route'; +import { camelRouteJson, camelRouteYaml } from '../../stubs/camel-route'; import { AddStepMode } from '../visualization/base-visual-entity'; import { CamelRouteVisualEntity } from '../visualization/flows/camel-route-visual-entity'; import { NonVisualEntity } from '../visualization/flows/non-visual-entity'; import { CamelComponentFilterService } from '../visualization/flows/support/camel-component-filter.service'; import { BeansEntity } from '../visualization/metadata/beansEntity'; -import { createCamelResource } from './camel-resource'; import { CamelRouteResource } from './camel-route-resource'; import { EntityType } from './entities'; import { SourceSchemaType } from './source-schema-type'; +import { CamelResourceFactory } from './camel-resource-factory'; +import { CamelYamlDsl } from '@kaoto/camel-catalog/types'; describe('CamelRouteResource', () => { it('should create CamelRouteResource', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); expect(resource.getType()).toEqual(SourceSchemaType.Route); expect(resource.getVisualEntities().length).toEqual(1); expect(resource.getEntities().length).toEqual(0); @@ -27,18 +28,29 @@ describe('CamelRouteResource', () => { }); describe('constructor', () => { - it.each([ - [camelRouteJson, CamelRouteVisualEntity], - [camelFromJson, CamelRouteVisualEntity], - [{ from: { uri: 'direct:foo', steps: [] } }, CamelRouteVisualEntity], - [{ from: 'direct:foo' }, NonVisualEntity], - [{ from: { uri: 'direct:foo' } }, CamelRouteVisualEntity], - [{ beans: [] }, BeansEntity], - [{}, NonVisualEntity], - [undefined, undefined], - [null, undefined], + const testCases: [CamelYamlDsl, unknown][] = [ + // Good cases + [[camelRouteJson], CamelRouteVisualEntity], + [[camelFromJson], CamelRouteVisualEntity], + [[{ from: { uri: 'direct:foo', steps: [] } }], CamelRouteVisualEntity], + [[{ from: { uri: 'direct:foo' } }] as CamelYamlDsl, CamelRouteVisualEntity], + [[{ beans: [] }], BeansEntity], [[], undefined], - ])('should return the appropriate entity for: %s', (json, expected) => { + + // Temporary good cases + [camelRouteJson as unknown as CamelYamlDsl, CamelRouteVisualEntity], + [camelFromJson as unknown as CamelYamlDsl, CamelRouteVisualEntity], + [{ from: { uri: 'direct:foo', steps: [] } } as unknown as CamelYamlDsl, CamelRouteVisualEntity], + [{ from: { uri: 'direct:foo' } } as unknown as CamelYamlDsl, CamelRouteVisualEntity], + [{ beans: [] } as unknown as CamelYamlDsl, BeansEntity], + + // Bad cases + [{ from: 'direct:foo' } as unknown as CamelYamlDsl, NonVisualEntity], + [{} as CamelYamlDsl, NonVisualEntity], + [undefined as unknown as CamelYamlDsl, undefined], + [null as unknown as CamelYamlDsl, undefined], + ]; + it.each(testCases)('should return the appropriate entity for: %s', (json, expected) => { const resource = new CamelRouteResource(json); const firstEntity = resource.getVisualEntities()[0] ?? resource.getEntities()[0]; @@ -98,14 +110,14 @@ describe('CamelRouteResource', () => { }); it('should return visual entities', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); expect(resource.getVisualEntities()).toHaveLength(1); expect(resource.getVisualEntities()[0]).toBeInstanceOf(CamelRouteVisualEntity); expect(resource.getEntities()).toHaveLength(0); }); it('should return entities', () => { - const resource = new CamelRouteResource(beansJson); + const resource = new CamelRouteResource([beansJson]); expect(resource.getEntities()).toHaveLength(1); expect(resource.getEntities()[0]).toBeInstanceOf(BeansEntity); expect(resource.getVisualEntities()).toHaveLength(0); @@ -113,7 +125,7 @@ describe('CamelRouteResource', () => { describe('toJSON', () => { it('should return JSON', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); expect(resource.toJSON()).toMatchSnapshot(); }); @@ -141,7 +153,7 @@ describe('CamelRouteResource', () => { describe('removeEntity', () => { it('should not do anything if the ID is not provided', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); resource.removeEntity(); @@ -149,7 +161,7 @@ describe('CamelRouteResource', () => { }); it('should not do anything when providing a non existing ID', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); resource.removeEntity('non-existing-id'); @@ -166,7 +178,7 @@ describe('CamelRouteResource', () => { }); it('should NOT create a new entity after deleting them all', () => { - const resource = new CamelRouteResource(camelRouteJson); + const resource = new CamelRouteResource([camelRouteJson]); const camelRouteEntity = resource.getVisualEntities()[0]; resource.removeEntity(camelRouteEntity.id); @@ -179,7 +191,7 @@ describe('CamelRouteResource', () => { it('should delegate to the CamelComponentFilterService', () => { const filterSpy = jest.spyOn(CamelComponentFilterService, 'getCamelCompatibleComponents'); - const resource = createCamelResource(camelRouteJson); + const resource = CamelResourceFactory.createCamelResource(camelRouteYaml); resource.getCompatibleComponents(AddStepMode.ReplaceStep, { path: 'from', label: 'timer' }); expect(filterSpy).toHaveBeenCalledWith(AddStepMode.ReplaceStep, { path: 'from', label: 'timer' }, undefined); @@ -187,39 +199,32 @@ describe('CamelRouteResource', () => { }); describe('toJson', () => { - it.each([ - [camelRouteJson], - [camelFromJson], - [{ from: { uri: 'direct:foo', steps: [] } }], - [{ from: 'direct:foo' }], - [{ from: { uri: 'direct:foo' } }], - [{ beans: [] }], - [{ errorHandler: [] }], - [{ intercept: {} }], - [{ interceptFrom: {} }], - [{ interceptSendToEndpoint: {} }], - [{ onCompletion: {} }], - [{ onException: {} }], - [{ rest: {} }], - [{ restConfiguration: {} }], - [{ route: {} }], - [{ routeConfiguration: {} }], - [{ routeTemplate: {} }], - [{ templatedRoute: {} }], - [{ anotherUnknownContent: {} }], - [{}], - ])('should not throw error when calling: %s', (json) => { + const testCases: [CamelYamlDsl][] = [ + [[camelRouteJson]], + [[camelFromJson]], + [[{ from: { uri: 'direct:foo', steps: [] } }]], + [{ from: 'direct:foo' } as unknown as CamelYamlDsl], + [{ from: { uri: 'direct:foo' } } as unknown as CamelYamlDsl], + [[{ beans: [] }]], + [[{ errorHandler: {} }]], + [[{ intercept: {} }]], + [[{ interceptFrom: {} }]], + [{ interceptSendToEndpoint: {} } as unknown as CamelYamlDsl], + [{ onCompletion: {} } as unknown as CamelYamlDsl], + [{ onException: {} } as unknown as CamelYamlDsl], + [{ rest: {} } as unknown as CamelYamlDsl], + [[{ restConfiguration: {} }]], + [{ route: {} } as unknown as CamelYamlDsl], + [[{ routeConfiguration: {} }]], + [[{ routeTemplate: {} }] as unknown as CamelYamlDsl], + [{ templatedRoute: {} } as unknown as CamelYamlDsl], + [{ anotherUnknownContent: {} } as unknown as CamelYamlDsl], + [{} as CamelYamlDsl], + ]; + it.each(testCases)('should not throw error when calling: %s', (json) => { const resource = new CamelRouteResource(json); const firstEntity = resource.getVisualEntities()[0] ?? resource.getEntities()[0]; expect(firstEntity.toJSON()).not.toBeUndefined(); }); }); - - describe('comments', () => { - it('should set and get comments', () => { - const resource = new CamelRouteResource(); - resource.setComments(['a', 'b']); - expect(resource.getComments()).toEqual(['a', 'b']); - }); - }); }); diff --git a/packages/ui/src/models/camel/camel-route-resource.ts b/packages/ui/src/models/camel/camel-route-resource.ts index 13a1a7a2c..238d93865 100644 --- a/packages/ui/src/models/camel/camel-route-resource.ts +++ b/packages/ui/src/models/camel/camel-route-resource.ts @@ -1,4 +1,4 @@ -import { RouteDefinition } from '@kaoto/camel-catalog/types'; +import { CamelYamlDsl, RouteDefinition } from '@kaoto/camel-catalog/types'; import { TileFilter } from '../../components/Catalog'; import { createCamelPropertiesSorter, isDefined } from '../../utils'; import { CatalogKind } from '../catalog-kind'; @@ -20,6 +20,8 @@ import { BeansEntity, isBeans } from '../visualization/metadata'; import { BaseVisualCamelEntityDefinition, BeansAwareResource, CamelResource } from './camel-resource'; import { BaseCamelEntity, EntityType } from './entities'; import { SourceSchemaType } from './source-schema-type'; +import { CamelResourceSerializer } from '../../serializers/camel-resource-serializer'; +import { YamlCamelResourceSerializer } from '../../serializers'; export class CamelRouteResource implements CamelResource, BeansAwareResource { static readonly SUPPORTED_ENTITIES: { type: EntityType; group: string; Entity: BaseVisualCamelEntityConstructor }[] = @@ -46,12 +48,14 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { ) => number; private entities: BaseCamelEntity[] = []; private resolvedEntities: BaseVisualCamelEntityDefinition | undefined; - private comments: string[] = []; + private serializer: CamelResourceSerializer; - constructor(json?: unknown) { - if (!json) return; - const rawEntities = Array.isArray(json) ? json : [json]; - this.entities = rawEntities.reduce((acc, rawItem) => { + constructor(rawEntities?: CamelYamlDsl, serializer?: CamelResourceSerializer) { + this.serializer = serializer ?? new YamlCamelResourceSerializer(); + if (!rawEntities) return; + + const entities = Array.isArray(rawEntities) ? rawEntities : [rawEntities]; + this.entities = entities.reduce((acc, rawItem) => { const entity = this.getEntity(rawItem); if (isDefined(entity) && typeof entity === 'object') { acc.push(entity); @@ -89,6 +93,16 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { return this.resolvedEntities; } + getSerializer(): CamelResourceSerializer { + return this.serializer; + } + + setSerializer(serializer: CamelResourceSerializer): void { + // Preserve comments + serializer.setComments(this.serializer.getComments()); + this.serializer = serializer; + } + addNewEntity(entityType?: EntityType): string { if (entityType && entityType !== EntityType.Route) { const supportedEntity = CamelRouteResource.SUPPORTED_ENTITIES.find(({ type }) => type === entityType); @@ -137,6 +151,10 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { return this.entities.map((entity) => entity.toJSON()); } + toString() { + return this.serializer.serialize(this); + } + createBeansEntity(): BeansEntity { const newBeans = { beans: [] }; const beansEntity = new BeansEntity(newBeans); @@ -170,14 +188,6 @@ export class CamelRouteResource implements CamelResource, BeansAwareResource { return CamelComponentFilterService.getCamelCompatibleComponents(mode, visualEntityData, definition); } - setComments(comments: string[]): void { - this.comments = comments; - } - - getComments(): string[] { - return this.comments; - } - private getEntity(rawItem: unknown): BaseCamelEntity | undefined { if (!isDefined(rawItem) || Array.isArray(rawItem)) { return undefined; diff --git a/packages/ui/src/models/camel/kamelet-resource.test.ts b/packages/ui/src/models/camel/kamelet-resource.test.ts index 3afde53f5..0588c765a 100644 --- a/packages/ui/src/models/camel/kamelet-resource.test.ts +++ b/packages/ui/src/models/camel/kamelet-resource.test.ts @@ -1,9 +1,9 @@ import { kameletJson } from '../../stubs/kamelet-route'; import { AddStepMode } from '../visualization/base-visual-entity'; import { CamelComponentFilterService } from '../visualization/flows/support/camel-component-filter.service'; -import { createCamelResource } from './camel-resource'; import { KameletResource } from './kamelet-resource'; import { SourceSchemaType } from './source-schema-type'; +import { CamelKResourceFactory } from './camel-k-resource-factory'; import { cloneDeep } from 'lodash'; describe('KameletResource', () => { @@ -78,7 +78,7 @@ describe('KameletResource', () => { it('should delegate to the CamelComponentFilterService', () => { const filterSpy = jest.spyOn(CamelComponentFilterService, 'getKameletCompatibleComponents'); - const resource = createCamelResource(kameletJson); + const resource = CamelKResourceFactory.getCamelKResource(kameletJson)!; resource.getCompatibleComponents(AddStepMode.ReplaceStep, { path: 'from', label: 'timer' }); expect(filterSpy).toHaveBeenCalledWith(AddStepMode.ReplaceStep, { path: 'from', label: 'timer' }, undefined); @@ -98,12 +98,4 @@ describe('KameletResource', () => { expect(model.spec.template.beans).toBeUndefined(); expect(kameletResource.getRouteTemplateBeansEntity()).toBeUndefined(); }); - - describe('comments', () => { - it('should set and get comments', () => { - const resource = new KameletResource(); - resource.setComments(['a', 'b']); - expect(resource.getComments()).toEqual(['a', 'b']); - }); - }); }); diff --git a/packages/ui/src/models/camel/pipe-resource.test.ts b/packages/ui/src/models/camel/pipe-resource.test.ts index c1607c008..101ba07ba 100644 --- a/packages/ui/src/models/camel/pipe-resource.test.ts +++ b/packages/ui/src/models/camel/pipe-resource.test.ts @@ -53,12 +53,4 @@ describe('PipeResource', () => { expect(resource.getEntities().length).toEqual(0); expect(resource.toJSON().metadata).toBeUndefined(); }); - - describe('comments', () => { - it('should set and get comments', () => { - const resource = new PipeResource(); - resource.setComments(['a', 'b']); - expect(resource.getComments()).toEqual(['a', 'b']); - }); - }); }); diff --git a/packages/ui/src/serializers/__snapshots__/yaml-camel-resource-serializer.test.ts.snap b/packages/ui/src/serializers/__snapshots__/yaml-camel-resource-serializer.test.ts.snap new file mode 100644 index 000000000..6b3d51d86 --- /dev/null +++ b/packages/ui/src/serializers/__snapshots__/yaml-camel-resource-serializer.test.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`YamlCamelResourceSerializer includes comments in YAML string 1`] = ` +"# comment1 + +# Comment2 +- route: + id: route-8888 + from: + uri: timer + parameters: + timerName: tutorial + steps: + - set-header: + name: myChoice + simple: \${random(2)} + - choice: + otherwise: + steps: + - to: + uri: "amqp:queue:" + - to: + uri: "amqp:queue:" + - log: + id: log-2 + message: We got a \${body} + when: + - steps: + - log: + id: log-1 + message: We got a one. + simple: \${header.myChoice} == 1 + - to: + uri: direct:my-route + parameters: + bridgeErrorHandler: true +" +`; diff --git a/packages/ui/src/serializers/camel-resource-serializer.ts b/packages/ui/src/serializers/camel-resource-serializer.ts new file mode 100644 index 000000000..34d22546f --- /dev/null +++ b/packages/ui/src/serializers/camel-resource-serializer.ts @@ -0,0 +1,9 @@ +import { CamelResource } from '../models/camel'; +import { CamelYamlDsl, Integration, Kamelet, KameletBinding, Pipe } from '@kaoto/camel-catalog/types'; + +export interface CamelResourceSerializer { + parse: (code: string) => CamelYamlDsl | Integration | Kamelet | KameletBinding | Pipe | undefined; + serialize: (resource: CamelResource) => string; + getComments: () => string[]; + setComments: (comments: string[]) => void; +} diff --git a/packages/ui/src/serializers/index.ts b/packages/ui/src/serializers/index.ts new file mode 100644 index 000000000..1ad677bad --- /dev/null +++ b/packages/ui/src/serializers/index.ts @@ -0,0 +1,3 @@ +export * from './camel-resource-serializer'; +export * from './xml-camel-resource-serializer'; +export * from './yaml-camel-resource-serializer'; diff --git a/packages/ui/src/serializers/xml-camel-resource-serializer.ts b/packages/ui/src/serializers/xml-camel-resource-serializer.ts new file mode 100644 index 000000000..6d9985965 --- /dev/null +++ b/packages/ui/src/serializers/xml-camel-resource-serializer.ts @@ -0,0 +1,27 @@ +import { CamelResource } from '../models/camel'; +import { CamelResourceSerializer } from './camel-resource-serializer'; +import { CamelYamlDsl, Integration, Kamelet, KameletBinding, Pipe } from '@kaoto/camel-catalog/types'; + +export class XmlCamelResourceSerializer implements CamelResourceSerializer { + static isApplicable(_code: unknown): boolean { + return false; + } + + parse(_code: string): CamelYamlDsl | Integration | Kamelet | KameletBinding | Pipe { + //TODO implement + return {}; + } + + serialize(_resource: CamelResource): string { + //TODO implement + return ''; + } + + getComments(): string[] { + return []; + } + + setComments(_comments: string[]): void { + //TODO implement + } +} diff --git a/packages/ui/src/serializers/yaml-camel-resource-serializer.test.ts b/packages/ui/src/serializers/yaml-camel-resource-serializer.test.ts new file mode 100644 index 000000000..9991cb242 --- /dev/null +++ b/packages/ui/src/serializers/yaml-camel-resource-serializer.test.ts @@ -0,0 +1,41 @@ +import { YamlCamelResourceSerializer } from './yaml-camel-resource-serializer'; +import { camelRouteJson, camelRouteYaml } from '../stubs'; +import { CamelRouteResource } from '../models/camel'; +import { CamelYamlDsl } from '@kaoto/camel-catalog/types'; + +describe('YamlCamelResourceSerializer', () => { + let serializer: YamlCamelResourceSerializer; + + beforeEach(() => { + serializer = new YamlCamelResourceSerializer(); + }); + + it('parses YAML code into JSON object', () => { + const result = serializer.parse(camelRouteYaml); + expect(result).toEqual([camelRouteJson]); + }); + + it('returns empty array for empty or non-string input in parse', () => { + expect(serializer.parse('')).toEqual([]); + expect(serializer.parse(null as unknown as string)).toEqual([]); + expect(serializer.parse(123 as unknown as string)).toEqual([]); + }); + + it('includes comments in serialized YAML string', () => { + const entities = serializer.parse('# comment1\n' + camelRouteYaml); + expect(serializer.comments.includes('# comment1')).toBeTruthy(); + + serializer.comments.push('# Comment2'); + const result = serializer.serialize(new CamelRouteResource(entities as CamelYamlDsl)); + expect(result).toContain('# Comment2'); + }); + + it('includes comments in YAML string', () => { + const entities = serializer.parse('# comment1\n' + camelRouteYaml); + expect(serializer.comments.includes('# comment1')).toBeTruthy(); + + serializer.comments.push('# Comment2'); + const result = serializer.serialize(new CamelRouteResource(entities as CamelYamlDsl)); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/ui/src/serializers/yaml-camel-resource-serializer.ts b/packages/ui/src/serializers/yaml-camel-resource-serializer.ts new file mode 100644 index 000000000..aad2b1ab1 --- /dev/null +++ b/packages/ui/src/serializers/yaml-camel-resource-serializer.ts @@ -0,0 +1,64 @@ +import { CamelResource } from '../models/camel'; +import { parse, stringify } from 'yaml'; +import { CamelResourceSerializer } from './camel-resource-serializer'; +import { CamelYamlDsl, Integration, Kamelet, KameletBinding, Pipe } from '@kaoto/camel-catalog/types'; + +export class YamlCamelResourceSerializer implements CamelResourceSerializer { + /** + * Regular expression to match commented lines, regardless of indentation + * Given the following examples, the regular expression should match the comments: + * ``` + * # This is a comment + * # This is an indented comment + *# This is an indented comment + * ``` + * The regular expression should match the first three lines + */ + static readonly COMMENTED_LINES_REGEXP = /^\s*#.*$/; + comments: string[] = []; + + static isApplicable(_code: unknown): boolean { + //TODO + // return !isXML(code); + + return true; + } + + parse(code: string): CamelYamlDsl | Integration | Kamelet | KameletBinding | Pipe { + if (!code || typeof code !== 'string') return []; + + this.comments = this.parseComments(code); + const json = parse(code); + return json; + } + + serialize(resource: CamelResource): string { + let code = stringify(resource, { sortMapEntries: resource.sortFn, schema: 'yaml-1.1' }) || ''; + if (this.comments.length > 0) { + const comments = this.comments.join('\n'); + code = comments + '\n' + code; + } + return code; + } + + getComments(): string[] { + return this.comments; + } + + setComments(comments: string[]): void { + this.comments = comments; + } + + private parseComments(code: string): string[] { + const lines = code.split('\n'); + const comments: string[] = []; + for (const line of lines) { + if (line.trim() === '' || YamlCamelResourceSerializer.COMMENTED_LINES_REGEXP.test(line)) { + comments.push(line); + } else { + break; + } + } + return comments; + } +} diff --git a/packages/ui/src/stubs/TestProvidersWrapper.tsx b/packages/ui/src/stubs/TestProvidersWrapper.tsx index dfead3956..9963905bf 100644 --- a/packages/ui/src/stubs/TestProvidersWrapper.tsx +++ b/packages/ui/src/stubs/TestProvidersWrapper.tsx @@ -19,7 +19,7 @@ interface TestProvidersWrapperResult { } export const TestProvidersWrapper = (props: TestProviderWrapperProps = {}): TestProvidersWrapperResult => { - const camelResource = props.camelResource ?? new CamelRouteResource(camelRouteJson); + const camelResource = props.camelResource ?? new CamelRouteResource([camelRouteJson]); const currentSchemaType = camelResource.getType(); const setCurrentSchemaTypeSpy = jest.fn(); const updateEntitiesFromCamelResourceSpy = jest.fn();