BPMN DSL
This DSL will bring your domain into your BPMN-Process.
As the pattern is always the same, we can setup each BPMN Element in the same way.
object BpmnElement extends CompanyBpmn<Element>Dsl:
val processName = "mycompany-myproject-myelement" // depending on the element this can be different
lazy val descr = "my element..."
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
case class Out(...)
object Out:
given ApiSchema[Out] = deriveApiSchema
given InOutCodec[Out] = deriveInOutCodec
lazy val example = <bpmnElement>(
In(),
Out(),
)
end BpmnElement
So each BPMN Element has:
- id: a unique identifier, it must be unique within a Camunda Instance.
Depending on the element this is named differently, e.g.
processName
,messageName
etc. - descr: a description of this element.
- In: an input class with the input process variables that we descibed in the Domain Specification.
- Out: an output class with the output process variables that we descibed in the Domain Specification.
- example: a method that creates an example of this element.
Special Case Enums
If you have an Enum as an In or Out class, you need to define the example like this:
lazy val example = <bpmnElement>(
In.CaseA(),
Out.CaseA(),
).withEnumInExamples(In.CaseB())
.withEnumOutExamples(Out.CaseB())
- This is needed to document both cases and also to handle them correctly in the Workers.
We support the following elements:
Process
The Process is the main element of a BPMN. It is the most complex element and looks like this:
object MyProcess extends CompanyBpmnProcessDsl:
val processName = "mycompany-myproject-myprocess"
lazy val descr = "my process..."
case class In(
...
inConfig: Option[InConfig] = None
) extends WithConfig[InConfig]
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
case class InConfig(
// Process Configuration
...
// Mocks
... )
object InConfig:
given ApiSchema[InConfig] = deriveApiSchema
given InOutCodec[InConfig] = deriveInOutCodec
case class InitIn(...)
object InitIn:
given ApiSchema[InitIn] = deriveApiSchema
given InOutCodec[InitIn] = deriveInOutCodec
case class Out(...)
object Out:
given ApiSchema[Out] = deriveApiSchema
given InOutCodec[Out] = deriveInOutCodec
lazy val example = process(
In(),
Out(),
InitIn()
)
end MyProcess
Next to the In and Out classes we have an InitIn- and InConfig class.
InitIn
Each process has an InitWorker that is the first worker that is called when the process is started.
Use this class to:
- init the Process Variables that are needed in the process (e.g. counters, variables used in expressions that must be defined (Camunda 7 restriction)).
- init the Process Variables with default values, that are not provided by the client. So you can be sure that they are always set - from Option to required in the process.
- extract some information from the input variables to simplify the process.
InConfig
These are technical Process Variables, like:
- Control the process flow (e.g. timers).
- Mocking of services and sub-processes.
The InitWorker will automatically put these variables on the process. That means you can override them for example in Postman
- just set them as process variables (
inConfig
object is not needed).
Business Rule Tasks (Decision DMNs)
We support only Decision DMNs. The input is always a domain object (each field must be a simple value that matches a column of the dmn). As simple values we support:
- String
- Boolean
- Int
- Long
- Double
- java.util.Date
- java.time.LocalDateTime
- java.time.ZonedDateTime
- scala.reflect.Enum
A domain object is a case class as described in the Specification, with the exception, that each field must be a simple value that matches a column of the dmn.
Inputs are always domain objects.
In the DSL we have an element for each of the four different return types - so you don't mix up the types 😊.
singleEntry
This is a single result with one simple value.
singleEntry(
in = Input("A"),
out = 1
)
singleResult
This is a single result with more than one value (domain object).
singleResult(
in = Input("A"),
out = ManyOutResult(1, "🤩")
)
collectEntries
This is a list of simple values.
collectEntries(
in = Input("A"),
out = Seq(1, 2)
)
resultList
This is a list of domain objects.
resultList(
in = Input("A"),
out = List(ManyOutResult(1, "🤩"), ManyOutResult(2, "😂"))
)
User Task
A User Task describes its form values that it offers and the values it must be completed with.
object MyUserTask extends CompanyBpmnUserTaskDsl:
val name = "mycompany-myproject-myusertask"
val descr: String = "my user task..."
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
case class Out(...)
object Out:
given ApiSchema[Out] = deriveApiSchema
given InOutCodec[Out] = deriveInOutCodec
lazy val example = userTask(
In(),
Out()
)
end MyUserTask
- The
name
is the name of the user task, be aware at the moment this is only for documentation. - A UserTask extends CompanyBpmnUserTaskDsl.
- The
In
object are the input variables you expect for the UI-Form of the UserTask. - The
Out
object are the process variables, the UI-Form sends, when it completes the UserTask.
External Task
An External Task describes a worker. We distinguish different types of Tasks, which are described in the next subchapters.
Custom Task
A Custom Task is a description for a worker that does some business logic, like mapping.
In General, you can do whatever you want with a Custom Task. See also CustomWorker.
object MyCustomTask extends CompanyBpmnCustomTaskDsl:
val topicName = "mycompany-myproject-myprocessV1.MyCustomTask"
val descr: String = "my custom task..."
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
case class Out(...)
object Out:
given ApiSchema[Out] = deriveApiSchema
given InOutCodec[Out] = deriveInOutCodec
lazy val example = customTask(
In(),
Out()
)
end MyCustomTask
Init Task
An Init Task is a description for a worker that initializes a Process.
In General, you map the In
object to the InitIn
object like init process variables.
See also InitWorker.
So no extra BPMN Element is needed for this, it is automatically defined by the Process.
object MyProcess extends CompanyBpmnProcessDsl:
...
case class In(
...
inConfig: Option[InConfig] = None
) extends WithConfig[InConfig]
...
case class InitIn(...)
...
end MyProcess
Service Task
A Service Task is a description for a worker that provides a REST API request.
See also ServiceWorker.
object MyServiceTask extends CompanyBpmnServiceTaskDsl:
val topicName = "mycompany-myproject-myservicetask"
val descr: String = "my service task..."
val path = "POST: /myService"
type ServiceIn = MyServiceBody
type ServiceOut = NoOutput
lazy val serviceInExample = MyServiceBody()
lazy val serviceMock = MockedServiceResponse.success204
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
case class Out(...)
object Out:
given ApiSchema[Out] = deriveApiSchema
given InOutCodec[Out] = deriveInOutCodec
lazy val example = serviceTask(
In(),
Out(),
serviceMock,
serviceInExample
)
end MyServiceTask
This task is more specific, and so we need to define
- The method and the path of the REST service for the documentation.
- The ServiceIn and ServiceOut types. They represent the bodies of the service-request and -response.
- In the example we define also the serviceInExample and the serviceMock.
At the moment we only support these 3 types of External Tasks. Depending on the use case we can add more types.
Receive Message Event
A Receive Message Event represents a catching message event. The input defines the message you expect. This works only as intermediate event. As we don't support throwing Message events we can simplify this to messageEvent:
object MyMessageEvent extends CompanyBpmnMessageEventDsl:
val messageName = "mycompany-myproject-mymessage"
val descr: String = "my message..."
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
lazy val example = messageEvent(In())
end MyMessageEvent
- The
messageName
is the name of the message you expect. The correlation can be the business key or the process instance id. In the Simulation we will use the processInstanceId. - You can send process variables with the
In
object. - A MessageEvent extends CompanyBpmnMessageEventDsl.
Receive Signal Event
A Receive Signal Event represents a catching signal event. The input defines the signal you expect. This works only as intermediate event. As we don't support Throwing Signal events we can simplify this to signalEvent:
object MySignalEvent extends CompanyBpmnSignalEventDsl:
val messageName = "mycompany-myproject-mysignal-{processInstanceId}"
val descr: String = "my signal..."
case class In(...)
object In:
given ApiSchema[In] = deriveApiSchema
given InOutCodec[In] = deriveInOutCodec
lazy val example = signalEvent(In())
end MySignalEvent
- The
messageName
is the name of the signal you expect. To correlate the signal to a certain process instance, you can use the{processInstanceId}
as a part in the signal name. This will be replaced in the Simulation with the actual processInstanceId. - You can send process variables with the
In
object. - A SignalEvent extends CompanyBpmnSignalEventDsl.
Timer Event
A Timer Event represents a timer event. There is no input needed, you can use it to describe the timers in your API doc, or using them in the Simulations to execute the job of the timer immediately. This works only as intermediate event.
object MyTimerEvent extends CompanyBpmnTimerEventDsl:
val title = "mycompany-myproject-mytimer"
val descr: String = "my timer..."
lazy val example = timerEvent()
end MyTimerEvent