Skip to content

Commit

Permalink
fix: Refactor component schema generation
Browse files Browse the repository at this point in the history
* Moved component-catalog > JSON schema generation to the maven plugin
* Fixed array/object related issues in component schema (KaotoIO#448)
* Added default value handling for component schema
* Added an exhaustive test for rendering component configuration form (KaotoIO#449)
* Added `$comment` to hold the catalog `javaType`, supposed to be used for BeanReferenceField (KaotoIO#470)
  • Loading branch information
igarashitm committed Dec 2, 2023
1 parent 01d25cf commit 362a101
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 901 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;

/**
Expand Down Expand Up @@ -72,16 +73,72 @@ public Map<String, String> processCatalog() throws Exception {
* @throws Exception
*/
public String getComponentCatalog() throws Exception {
var answer = new LinkedHashMap<String, JsonObject>();
var answer = jsonMapper.createObjectNode();
api.findComponentNames().stream().sorted().forEach((name) -> {
try {
var model = (ComponentModel) api.model(Kind.component, name);
answer.put(name, JsonMapper.asJsonObject(model));
var json = JsonMapper.asJsonObject(model).toJson();
var catalogNode = (ObjectNode) jsonMapper.readTree(json);
generatePropertiesSchema(catalogNode);
answer.set(name, catalogNode);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return JsonMapper.serialize(answer);
StringWriter writer = new StringWriter();
var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter();
jsonMapper.writeTree(jsonGenerator, answer);
return writer.toString();
}

private void generatePropertiesSchema(ObjectNode parent) throws Exception {
var answer = parent.withObject("/propertiesSchema");
answer.put("$schema", "http://json-schema.org/draft-07/schema#");
answer.put("type", "object");

var properties = parent.withObject("/properties");
var answerProperties = answer.withObject("/properties");
var required = new LinkedHashSet<String>();
for (var propertyEntry : properties.properties()) {
var propertyName = propertyEntry.getKey();
var property = propertyEntry.getValue();
var propertySchema = answerProperties.withObject("/" + propertyName);
if (property.has("displayName")) propertySchema.put("title", property.get("displayName").asText());
if (property.has("description")) propertySchema.put("description", property.get("description").asText());
var propertyType = "string";
if (property.has("type")) {
propertyType = property.get("type").asText();
if ("duration".equals(propertyType)) {
propertyType = "string";
propertySchema.put("$comment", "duration");
}
propertySchema.put("type", propertyType);
}
if (property.has("deprecated")) propertySchema.put("deprecated", property.get("deprecated").asBoolean());
if (property.has("required") && property.get("required").asBoolean()) {
required.add(propertyName);
}
if (property.has("defaultValue")) {
if ("array".equals(propertyType)) {
propertySchema.withArray("/default").add(property.get("defaultValue"));
} else {
propertySchema.set("default", property.get("defaultValue"));
}
}

if (property.has("enum")) {
property.withArray("/enum")
.forEach(e -> propertySchema.withArray("/enum").add(e));
} else if ("array".equals(propertyType)) {
propertySchema.withObject("/items").put("type", "string");
} else if ("object".equals(propertyType) && property.has("javaType") && !property.get("javaType").asText().startsWith("java.util.Map")) {
// Put "string" as a type and javaType as a schema $comment to indicate
// that the UI should handle this as a bean reference field
propertySchema.put("type", "string");
propertySchema.put("$comment", "class:" + property.get("javaType").asText());
}
}
required.forEach(req -> answer.withArray("/required").add(req));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ public void testGetComponentCatalog() throws Exception {
.withObject("/direct")
.withObject("/component");
assertEquals("Direct", directModel.get("title").asText());
var as2Schema = componentCatalog
.withObject("/as2")
.withObject("/propertiesSchema");
var as2srmaProperty = as2Schema.withObject("/properties").withObject("/signedReceiptMicAlgorithms");
assertEquals("array", as2srmaProperty.get("type").asText());
assertEquals("string", as2srmaProperty.withObject("/items").get("type").asText());
var gdSchema = componentCatalog
.withObject("/google-drive")
.withObject("/propertiesSchema");
var gdScopesProperty = gdSchema.withObject("/properties").withObject("/scopes");
assertEquals("array", gdScopesProperty.get("type").asText());
assertEquals("string", gdScopesProperty.withObject("/items").get("type").asText());
var gdSPProperty = gdSchema.withObject("/properties").withObject("/schedulerProperties");
assertEquals("object", gdSPProperty.get("type").asText());
var sqlSchema = componentCatalog
.withObject("/sql")
.withObject("/propertiesSchema");
var sqlDSProperty = sqlSchema.withObject("/properties").withObject("/dataSource");
assertEquals("string", sqlDSProperty.get("type").asText());
assertEquals("class:javax.sql.DataSource", sqlDSProperty.get("$comment").asText());
var sqlBEHProperty = sqlSchema.withObject("/properties").withObject("/bridgeErrorHandler");
assertTrue(sqlBEHProperty.get("default").isBoolean());
assertFalse(sqlBEHProperty.get("default").asBoolean());
var etcdSchema = componentCatalog
.withObject("/etcd3")
.withObject("/propertiesSchema");
var etcdEProperty = etcdSchema.withObject("/properties").withObject("/endpoints");
assertEquals("Etcd3Constants.ETCD_DEFAULT_ENDPOINTS", etcdEProperty.withArray("/default").get(0).asText());
}

@Test
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/components/Form/CustomAutoField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export const CustomAutoField = createAutoField((props) => {
case Object:
return CustomNestField;
case String:
/* TODO Create BeanReferenceField - https://github.com/KaotoIO/kaoto-next/issues/470
catalog preprocessor put 'string' as a type and the javaType as a schema $comment
const comment = props['$comment'] as string;
if (comment?.startsWith('class:')) {
return BeanReferenceField;
}
*/
return TextField;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/components/Form/schema.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export class SchemaService {
getSchemaBridge(schema?: Record<string, unknown>): JSONSchemaBridge | undefined {
if (!schema) return undefined;

// uniforms passes it down to the React TextInput as an attribute, causes a warning
if (schema['$comment']) {
delete schema['$comment'];
}
const schemaValidator = this.createValidator(schema as JSONSchemaType<unknown>);

return new JSONSchemaBridge({ schema, validator: schemaValidator });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import { IVisualizationNode, VisualComponentSchema } from '../../../models/visua
import { EntitiesContext } from '../../../providers/entities.provider';
import { CanvasForm } from './CanvasForm';
import { CanvasNode } from './canvas.models';
import { AutoFields } from '@kaoto-next/uniforms-patternfly';
import { AutoForm } from 'uniforms';
import { CustomAutoField } from '../../Form/CustomAutoField';
import * as componentCatalogMap from '@kaoto-next/camel-catalog/camel-catalog-aggregate-components.json';
import { SchemaService } from '../../Form';

describe('CanvasForm', () => {
const omitFields = ['expression', 'dataFormatType', 'outputs', 'steps', 'when', 'otherwise', 'doCatch', 'doFinally'];

const schema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -116,4 +123,23 @@ describe('CanvasForm', () => {

expect(visualComponentSchema.definition.parameters).toEqual({});
});

it('should render for all component without an error', () => {
const schemaService = new SchemaService();
Object.entries(componentCatalogMap).forEach(([name, catalog]) => {
try {
if (name === 'default') return;
/* eslint-disable @typescript-eslint/no-explicit-any */
const schema = schemaService.getSchemaBridge((catalog as any).propertiesSchema);
render(
<AutoForm schema={schema!} model={{}} onChangeModel={() => {}}>
<AutoFields autoField={CustomAutoField} omitFields={omitFields} />
</AutoForm>,
);
} catch (e) {
/* eslint-disable @typescript-eslint/no-explicit-any */
throw new Error(`Error rendering ${name} component: ${(e as any).message}`);
}
});
});
});
2 changes: 2 additions & 0 deletions packages/ui/src/models/camel-components-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CamelPropertyCommon } from './camel-properties-common';
import { CatalogKind } from './catalog-kind';
import { JSONSchemaType } from 'ajv';

export interface ICamelComponentDefinition {
component: ICamelComponent;
componentProperties: Record<string, ICamelComponentProperty>;
properties: Record<string, ICamelComponentProperty>;
propertiesSchema: JSONSchemaType<unknown>;
headers?: Record<string, ICamelComponentHeader>;
apis?: Record<string, ICamelComponentApi>;
apiProperties?: Record<string, ICamelComponentApiProperty>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should build the a
"description": "Endpoint properties description",
"properties": {
"exchangeFormatter": {
"default": undefined,
"$comment": "class:org.apache.camel.spi.ExchangeFormatter",
"deprecated": false,
"description": "To use a custom exchange formatter",
"title": "Exchange Formatter",
"type": "object",
"type": "string",
},
"groupActiveOnly": {
"default": "true",
Expand All @@ -52,21 +52,18 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should build the a
"type": "boolean",
},
"groupDelay": {
"default": undefined,
"deprecated": false,
"description": "Set the initial delay for stats (in millis)",
"title": "Group Delay",
"type": "integer",
},
"groupInterval": {
"default": undefined,
"deprecated": false,
"description": "If specified will group message stats by this time interval (in millis)",
"title": "Group Interval",
"type": "integer",
},
"groupSize": {
"default": undefined,
"deprecated": false,
"description": "An integer that specifies a group size for throughput logging.",
"title": "Group Size",
Expand All @@ -92,24 +89,21 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should build the a
"OFF",
],
"title": "Level",
"type": undefined,
"type": "string",
},
"logMask": {
"default": undefined,
"deprecated": false,
"description": "If true, mask sensitive information like password or passphrase in the log.",
"title": "Log Mask",
"type": "boolean",
},
"loggerName": {
"default": undefined,
"deprecated": false,
"description": "Name of the logging category to use",
"title": "Logger Name",
"type": "string",
},
"marker": {
"default": undefined,
"deprecated": false,
"description": "An optional Marker name to use.",
"title": "Marker",
Expand Down Expand Up @@ -265,7 +259,7 @@ exports[`CamelComponentSchemaService getVisualComponentSchema should build the a
"Fixed",
],
"title": "Style",
"type": undefined,
"type": "object",
},
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { ProcessorDefinition } from '@kaoto-next/camel-catalog/types';
import { CatalogKind } from '../../..';
import { beerSourceKamelet } from '../../../../stubs/beer-source-kamelet';
import { logComponent } from '../../../../stubs/log-component';
import { logModel } from '../../../../stubs/log-model';
import { timerComponent } from '../../../../stubs/timer-component';
import { toModel } from '../../../../stubs/to-model';
import { CamelCatalogService } from '../camel-catalog.service';
import { CamelComponentSchemaService } from './camel-component-schema.service';
import * as componentCatalogMap from '@kaoto-next/camel-catalog/camel-catalog-aggregate-components.json';

describe('CamelComponentSchemaService', () => {
beforeEach(() => {
CamelCatalogService.setCatalogKey(CatalogKind.Component, {
log: logComponent,
timer: timerComponent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
log: (componentCatalogMap as any)['log'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timer: (componentCatalogMap as any)['timer'],
});
CamelCatalogService.setCatalogKey(CatalogKind.Processor, {
log: logModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class CamelComponentSchemaService {
componentSchema = NodeDefinitionService.getSchemaFromKameletDefinition(componentDefinition);
} else {
componentDefinition = CamelCatalogService.getComponent(CatalogKind.Component, camelElementLookup.componentName);
componentSchema = NodeDefinitionService.getSchemaFromCamelCommonProperties(componentDefinition?.properties);
componentSchema = componentDefinition?.propertiesSchema ?? ({} as unknown as JSONSchemaType<unknown>);
}

if (componentDefinition !== undefined && componentSchema !== undefined) {
Expand Down
Loading

0 comments on commit 362a101

Please sign in to comment.