Skip to content

Commit

Permalink
feat: generated REST connector template (#1094)
Browse files Browse the repository at this point in the history
* feat(rest): enable template generation for REST connector
* feat(generator): support hybrid templates
  • Loading branch information
chillleader authored Sep 28, 2023
1 parent e4cfae3 commit 9ae1aa2
Show file tree
Hide file tree
Showing 22 changed files with 1,128 additions and 798 deletions.
40 changes: 40 additions & 0 deletions connector-sdk/element-template-generator-maven-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,43 @@ by adding the following configuration to the plugin:
```

Note that the plugin will not resolve transitive dependencies of the specified dependencies.

## Hybrid mode

If you want to generate an element template for a Connector that should work in Hybrid mode
(multiple runtimes running against the same Camunda cluster), you can use the
`generate-hybrid-templates` parameter:

```xml
<configuration>
<connectorClasses>
<connectorClass>io.camunda.connector.MyConnector</connectorClass>
</connectorClasses>
<generateHybridTemplates>true</generateHybridTemplates>
</configuration>
```

The default value is `false`.

Note that if this parameter is set to `true`, both element templates (normal one and hybrid one)
will be generated.

## Custom file name

By default, the generated element template will be stored in a file with a name derived from
the element template ID (configurable via `@ElementTemplate` annotation). You can override
the file name by setting the `templateFileName` configuration parameter:

```xml
<configuration>
<connectorClasses>
<connectorClass>io.camunda.connector.MyConnector</connectorClass>
</connectorClasses>
<templateFileName>my-custom-template</templateFileName>
</configuration>
```

In this case, the generated element template will be stored in `my-custom-template.json`.

If used together with `generateHybridTemplates`, the file name of the hybrid template will be
`my-custom-template-hybrid.json`.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package io.camunda.connector.generator;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.connector.generator.core.GeneratorConfiguration;
import io.camunda.connector.generator.core.GeneratorConfiguration.ConnectorMode;
import io.camunda.connector.generator.core.OutboundElementTemplateGenerator;
import io.camunda.connector.generator.dsl.OutboundElementTemplate;
import java.io.File;
Expand Down Expand Up @@ -53,6 +55,12 @@ public class ElementTemplateGeneratorMojo extends AbstractMojo {
@Parameter(property = "outputDirectory", defaultValue = "${project.basedir}/element-templates")
private String outputDirectory;

@Parameter(property = "templateFileName")
private String templateFileName;

@Parameter(property = "generateHybridTemplates", defaultValue = "false")
private boolean generateHybridTemplates;

private static final ObjectMapper mapper = new ObjectMapper();
private OutboundElementTemplateGenerator generator;

Expand Down Expand Up @@ -104,8 +112,22 @@ public void execute() throws MojoFailureException {
for (String className : connectorClasses) {
getLog().info("Generating element template for " + className);
Class<?> clazz = classLoader.loadClass(className);
OutboundElementTemplate template = generateElementTemplate(clazz);
writeElementTemplate(template);
OutboundElementTemplate template = generator.generate(clazz);

var basicFileName =
templateFileName == null
? transformConnectorNameToTemplateFileName(template.name())
: templateFileName + ".json";

writeElementTemplate(template, basicFileName);

if (generateHybridTemplates) {
getLog().info("Generating hybrid element template for " + className);
OutboundElementTemplate hybridTemplate =
generator.generate(clazz, new GeneratorConfiguration(ConnectorMode.HYBRID));
var name = basicFileName.replace(".json", "-hybrid.json");
writeElementTemplate(hybridTemplate, name);
}
}

} catch (ClassNotFoundException e) {
Expand All @@ -120,13 +142,8 @@ public void execute() throws MojoFailureException {
}
}

private OutboundElementTemplate generateElementTemplate(Class<?> clazz) {
return generator.generate(clazz);
}

private void writeElementTemplate(OutboundElementTemplate template) {
private void writeElementTemplate(OutboundElementTemplate template, String fileName) {
try {
String fileName = transformConnectorNameToTemplateFileName(template.name());
File file = new File(outputDirectory, fileName);
file.getParentFile().mkdirs();
mapper.writerWithDefaultPrettyPrinter().writeValue(file, template);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,20 @@
*/
public interface ElementTemplateGenerator<T extends ElementTemplateBase> {

/**
* Generate Connector using default configuration
*
* @param connectorDefinition the connector definition class
* @return the generated element template
*/
T generate(Class<?> connectorDefinition);

/**
* Generate Connector using custom configuration
*
* @param connectorDefinition the connector definition class
* @param configuration the generator configuration
* @return the generated element template
*/
T generate(Class<?> connectorDefinition, GeneratorConfiguration configuration);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.camunda.connector.generator.core;

/** Configuration for the element template generator */
public record GeneratorConfiguration(
/*
* Connectors in hybrid mode have a configurable task definition type (for outbound), or a
* configurable connector type (for inbound) property. This allows to run multiple connector
* runtimes against the same Camunda cluster and distiguish between them on the BPMN level.
*/
ConnectorMode connectorMode) {
public enum ConnectorMode {
NORMAL,
HYBRID
}

public static final GeneratorConfiguration DEFAULT =
new GeneratorConfiguration(ConnectorMode.NORMAL);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.camunda.connector.api.annotation.OutboundConnector;
import io.camunda.connector.generator.annotation.ElementTemplate;
import io.camunda.connector.generator.core.GeneratorConfiguration.ConnectorMode;
import io.camunda.connector.generator.core.util.ReflectionUtil;
import io.camunda.connector.generator.core.util.TemplatePropertiesUtil;
import io.camunda.connector.generator.dsl.CommonProperties;
Expand All @@ -38,9 +39,20 @@ public class OutboundElementTemplateGenerator
implements ElementTemplateGenerator<OutboundElementTemplate> {

private final ClassLoader classLoader;
private final GeneratorConfiguration defaultConfiguration;

public OutboundElementTemplateGenerator(ClassLoader classLoader) {
public OutboundElementTemplateGenerator(
ClassLoader classLoader, GeneratorConfiguration configuration) {
this.classLoader = classLoader;
this.defaultConfiguration = configuration;
}

public OutboundElementTemplateGenerator(ClassLoader classLoader) {
this(classLoader, GeneratorConfiguration.DEFAULT);
}

public OutboundElementTemplateGenerator(GeneratorConfiguration configuration) {
this(Thread.currentThread().getContextClassLoader(), configuration);
}

public OutboundElementTemplateGenerator() {
Expand All @@ -49,6 +61,13 @@ public OutboundElementTemplateGenerator() {

@Override
public OutboundElementTemplate generate(Class<?> connectorDefinition) {
return generate(connectorDefinition, defaultConfiguration);
}

@Override
public OutboundElementTemplate generate(
Class<?> connectorDefinition, GeneratorConfiguration configuration) {

var connector =
ReflectionUtil.getRequiredAnnotation(connectorDefinition, OutboundConnector.class);
var template = ReflectionUtil.getRequiredAnnotation(connectorDefinition, ElementTemplate.class);
Expand Down Expand Up @@ -142,7 +161,7 @@ public OutboundElementTemplate generate(Class<?> connectorDefinition) {

return OutboundElementTemplate.builder()
.id(template.id())
.type(connector.type())
.type(connector.type(), ConnectorMode.HYBRID.equals(configuration.connectorMode()))
.name(template.name())
.version(template.version())
.icon(icon)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ private Integer hasMaxSizeAnnotation(Field field) {
private Pair<String, String> hasPatternAnnotation(Field field) {
var patternAnnotation = field.getAnnotation(Pattern.class);
if (patternAnnotation != null) {
if (patternAnnotation.message().equals("{jakarta.validation.constraints.Pattern.message}")) {
return Pair.of(patternAnnotation.regexp(), null);
}
return Pair.of(patternAnnotation.regexp(), patternAnnotation.message());
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,22 @@ public class CommonProperties {
TextProperty.builder()
.id("resultExpression")
.group("output")
.label("Result Expression")
.label("Result expression")
.description("Expression to map the response into process variables")
.feel(FeelMode.required);

public static final PropertyBuilder RESULT_VARIABLE =
StringProperty.builder()
.id("resultVariable")
.group("output")
.label("Result Variable")
.label("Result variable")
.description("Name of variable to store the response in")
.feel(FeelMode.disabled);

public static final PropertyBuilder ERROR_EXPRESSION =
TextProperty.builder()
.id("errorExpression")
.label("Error Expression")
.label("Error expression")
.group("error")
.description(
"Expression to handle errors. Details in the <a href=\"https://docs.camunda.io/docs/components/connectors/use-connectors/\" target=\"_blank\">documentation</a>.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package io.camunda.connector.generator.dsl;

import io.camunda.connector.generator.dsl.Property.FeelMode;
import io.camunda.connector.generator.dsl.PropertyBinding.ZeebeTaskDefinitionType;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -44,18 +45,35 @@ public OutboundElementTemplateBuilder id(String id) {
return this;
}

public OutboundElementTemplateBuilder type(String type) {
public OutboundElementTemplateBuilder type(String type, boolean configurable) {
if (isTypeAssigned()) {
throw new IllegalStateException("type is already assigned");
}
properties.add(
HiddenProperty.builder()
.value(type)
.binding(PropertyBinding.ZeebeTaskDefinitionType.INSTANCE)
.build());
Property property;
if (configurable) {
groups.add(
0,
PropertyGroup.builder().id("taskDefinitionType").label("Task definition type").build());
property =
StringProperty.builder()
.binding(ZeebeTaskDefinitionType.INSTANCE)
.value(type)
.id("taskDefinitionType")
.group("taskDefinitionType")
.feel(FeelMode.disabled)
.build();
} else {
property =
HiddenProperty.builder().binding(ZeebeTaskDefinitionType.INSTANCE).value(type).build();
}
properties.add(property);
return this;
}

public OutboundElementTemplateBuilder type(String type) {
return type(type, false);
}

public OutboundElementTemplateBuilder name(String name) {
this.name = name;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
@JsonInclude(Include.NON_NULL)
public record PropertyConstraints(
Boolean notEmpty, Integer minLength, Integer maxLength, Pattern pattern) {

@JsonInclude(Include.NON_NULL)
public record Pattern(String value, String message) {}

public static PropertyConstraintsBuilder builder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

import io.camunda.connector.generator.core.GeneratorConfiguration.ConnectorMode;
import io.camunda.connector.generator.core.example.MyConnectorFunction;
import io.camunda.connector.generator.dsl.BpmnType;
import io.camunda.connector.generator.dsl.DropdownProperty;
Expand Down Expand Up @@ -93,7 +94,7 @@ void elementTemplateAnnotation_providesCorrectDefaultValues() {
@Test
void resultVariableProperty() {
var template = generator.generate(MyConnectorFunction.MinimallyAnnotated.class);
var property = getPropertyByLabel("Result Variable", template);
var property = getPropertyByLabel("Result variable", template);
assertThat(property.getType()).isEqualTo("String");
assertThat(property.getBinding().type()).isEqualTo("zeebe:taskHeader");
assertThat(property.getFeel()).isNull();
Expand All @@ -102,7 +103,7 @@ void resultVariableProperty() {
@Test
void resultExpressionProperty() {
var template = generator.generate(MyConnectorFunction.MinimallyAnnotated.class);
var property = getPropertyByLabel("Result Expression", template);
var property = getPropertyByLabel("Result expression", template);
assertThat(property.getType()).isEqualTo("Text");
assertThat(property.getBinding().type()).isEqualTo("zeebe:taskHeader");
assertThat(property.getFeel()).isEqualTo(FeelMode.required);
Expand All @@ -111,7 +112,7 @@ void resultExpressionProperty() {
@Test
void errorExpressionProperty() {
var template = generator.generate(MyConnectorFunction.MinimallyAnnotated.class);
var property = getPropertyByLabel("Error Expression", template);
var property = getPropertyByLabel("Error expression", template);
assertThat(property.getType()).isEqualTo("Text");
assertThat(property.getBinding().type()).isEqualTo("zeebe:taskHeader");
assertThat(property.getFeel()).isEqualTo(FeelMode.required);
Expand All @@ -127,6 +128,18 @@ void retryBackoffProperty() {
assertThat(property.getGroup()).isEqualTo("retries");
assertThat(property.getValue()).isEqualTo("PT0S");
}

@Test
void hybridMode_taskDefinitionTypePropertyPresent() {
var template =
generator.generate(
MyConnectorFunction.MinimallyAnnotated.class,
new GeneratorConfiguration(ConnectorMode.HYBRID));
var property = getPropertyById("taskDefinitionType", template);
assertThat(property.getType()).isEqualTo("String");
assertThat(property.getGroup()).isEqualTo("taskDefinitionType");
assertThat(property.getFeel()).isEqualTo(null);
}
}

@Nested
Expand Down Expand Up @@ -344,6 +357,24 @@ void propertyGroupContents_definedByTemplatePropertyAnnotation() {
getPropertyByLabel("Annotated and renamed string property", template),
getPropertyByLabel("Property for group 1", template));
}

@Test
void hybridMode_groupPresentAndIsOnTop() {
var template =
generator.generate(
MyConnectorFunction.MinimallyAnnotated.class,
new GeneratorConfiguration(ConnectorMode.HYBRID));
checkPropertyGroups(
List.of(
Map.entry("taskDefinitionType", "Task definition type"),
Map.entry("group2", "Group 2"),
Map.entry("group1", "Group 1"),
Map.entry("output", "Output mapping"),
Map.entry("error", "Error handling"),
Map.entry("retries", "Retries")),
template,
true);
}
}

@Nested
Expand Down
Loading

0 comments on commit 9ae1aa2

Please sign in to comment.