Skip to content

Commit

Permalink
refactor(entities,resources): Decouple parsing from entities hook int…
Browse files Browse the repository at this point in the history
…o separate serializer class

 * create CamelResourceSerializer interface and YamlCamelResourceSerializer implementantion
 * create CamelResourceFactory and CamelKResourceFactory to creating new resources
 * move parsing out of the entities hook
  • Loading branch information
mmelko committed Nov 28, 2024
1 parent 6590a2d commit 0d5946e
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 144 deletions.
34 changes: 8 additions & 26 deletions packages/ui/src/hooks/entities.ts
Original file line number Diff line number Diff line change
@@ -1,9 +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';
import { CamelResourceFactory } from '../models/camel/camel-resource-factory';

/**
* Regular expression to match commented lines, regardless of indentation
Expand All @@ -15,7 +16,6 @@ import { EventNotifier } from '../utils';
* ```
* The regular expression should match the first three lines
*/
const COMMENTED_LINES_REGEXP = /^\s*#.*$/;

export interface EntitiesContextResult {
entities: BaseCamelEntity[];
Expand Down Expand Up @@ -49,7 +49,7 @@ export interface EntitiesContextResult {

export const useEntities = (): EntitiesContextResult => {
const eventNotifier = EventNotifier.getInstance();
const [camelResource, setCamelResource] = useState<CamelResource>(createCamelResource());
const [camelResource, setCamelResource] = useState<CamelResource>(CamelResourceFactory.createCamelResource());
const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);

Expand All @@ -58,36 +58,18 @@ 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);
});
}, [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]);

Expand All @@ -105,7 +87,7 @@ export const useEntities = (): EntitiesContextResult => {

const setCurrentSchemaType = useCallback(
(type: SourceSchemaType) => {
setCamelResource(createCamelResource(type));
setCamelResource(CamelResourceFactory.createCamelResource(type));
updateEntitiesFromCamelResource();
},
[updateEntitiesFromCamelResource],
Expand Down
33 changes: 33 additions & 0 deletions packages/ui/src/models/camel/camel-k-resource-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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?: unknown, type?: SourceSchemaType): CamelResource | undefined {
const jsonRecord = json as Record<string, unknown>;
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);
default:
return undefined;
}
}
return undefined;
}
}
25 changes: 17 additions & 8 deletions packages/ui/src/models/camel/camel-k-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,14 +26,19 @@ export const CAMEL_K_K8S_API_VERSION_V1 = 'camel.apache.org/v1';

export abstract class CamelKResource implements CamelResource {
static readonly PARAMETERS_ORDER = ['apiVersion', 'kind', 'metadata', 'spec', 'source', 'steps', 'sink'];
// static serializer = new YamlResourceSerializer();
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(),
) {
this.serializer = serializer;

if (parsedResource) {
this.resource = parsedResource as CamelKType;
} else {
this.resource = {
apiVersion: CAMEL_K_K8S_API_VERSION_V1,
Expand Down Expand Up @@ -94,12 +100,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);
}
}
27 changes: 27 additions & 0 deletions packages/ui/src/models/camel/camel-resource-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SourceSchemaType } from './source-schema-type';
import { CamelResource } from './camel-resource';
import { XmlCamelResourceSerializer, YamlCamelResourceSerializer } from '../../serializers';
import { CamelRouteResource } from './camel-route-resource';
import { CamelKResourceFactory } from './camel-k-resource-factory';

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 {
if (XmlCamelResourceSerializer.isApplicable(source)) {
return new CamelRouteResource(source, new XmlCamelResourceSerializer());
}

const serializer = new YamlCamelResourceSerializer();
const resource = CamelKResourceFactory.getCamelKResource(serializer.parse(source ?? ''), type);

if (resource) return resource;
return new CamelRouteResource(source, serializer);
}
}
28 changes: 14 additions & 14 deletions packages/ui/src/models/camel/camel-resource.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
Expand All @@ -64,27 +64,27 @@ 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(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;
expect(vis.pipe.spec?.source?.ref?.name).toEqual('webhook-source');
});

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;
Expand Down
49 changes: 4 additions & 45 deletions packages/ui/src/models/camel/camel-resource.ts
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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(
Expand All @@ -35,8 +28,6 @@ export interface CamelResource {
): TileFilter | undefined;

sortFn?: (a: unknown, b: unknown) => number;
setComments(comments: string[]): void;
getComments(): string[];
}

export interface BaseVisualCamelEntityDefinition {
Expand All @@ -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<string, unknown>;
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);
}
}
Loading

0 comments on commit 0d5946e

Please sign in to comment.