OpenAPI API Configuration
The OpenAPI integration in SmartWeb works in two directions:
Inbound OpenAPI — SmartWeb exposes external HTTP endpoints described by an OpenAPI specification. Each operation is bound to a D2000 RPC procedure. Incoming HTTP requests are translated into D2000 RPC calls.
Outbound OpenAPI — D2000 ESL scripts can, through a connector running inside SmartWeb, call external HTTP services described by an OpenAPI specification. The connector serializes the RPC arguments into JSON, sends the request and returns the deserialized response back to D2000.
This page covers both directions. Generating the TypeScript client for D2000 RPC is no longer part of OpenAPI — see Generating a TypeScript Client for D2000 RPC.
Inbound OpenAPI
Inbound OpenAPI is exposed at /api/open and secured the same way as the server-to-server REST API (API key, JWT or Basic auth). On startup SmartWeb parses every configured specification file and publishes its POST operations at /api/open/ + contextPath + the path from the specification. Each operation is bound to a D2000 RPC procedure named by the x-d2-* extensions of the specification (see Nested structures); other HTTP methods are not bound, because an RPC call always carries a payload.
GET /api/open/<contextPath>/openapi.yaml returns the document that is really deployed — including the defaults SmartWeb filled in and any operations generated from D2000 metadata. For the example below that is GET /api/open/edc/openapi.yaml. The report of what SmartWeb made of the specifications and what it is missing in D2000 is at GET /api/open/status.md — see Generated specification documentation.
Configuration
smartweb:
application:
openApi:
enabled: true
processName: "SELF.SMARTWEB_OPENAPI" # only needed by a specification with a "mapping" block
httpHeaders: [ ] # extra HTTP headers added to /api/open responses
specifications:
- specificationFilePath: edc_openapi_ofs.yaml # relative to the configuration directory
specificationFormat: YAML # YAML | JSON
specificationVersion: "3.0"
contextPath: edc # URL segment under /api/open
generateDocumentation: true # on by default, see the end of this pageProperty | Description |
|---|---|
| Switches the inbound API on or off. A call to a disabled API is answered with |
| D2000 process the metadata session logs on as. Required only by a specification with a |
| Extra HTTP headers added to |
| Path to the specification file, relative to the configuration directory. The file is watched — a change reloads the context, and a file that fails to parse leaves the previously loaded version deployed. |
|
|
| URL segment the operations are published under. Must not be empty — a specification without it is never mounted. |
|
|
| Optional — generates operations from D2000 RPC metadata, see below. |
[!NOTE]
The URL is /api/open + contextPath + the path written in the specification. With contextPath: edc and a path /ofs/api/v1/contracts-changes the operation is called at POST /api/open/edc/ofs/api/v1/contracts-changes.
Request flow
An external client sends
POST /api/open/edc/ofs/api/v1/contracts-changes.SmartWeb resolves the URL to the specification and the operation bound to that path.
The properties of the request body become the RPC input parameters, in the depth-first order of the payload schema; a property can also be filled from an HTTP header (see below).
The output parameters are appended as empty values, so the ESL procedure receives the whole signature.
The bound D2000 RPC procedure —
x-d2-event-name+x-d2-event-rpc-name— is called, by default as a conversation (CONVERSATION_BEGIN_END), and SmartWeb waits for the ESL answer.The output parameters are serialized back into JSON according to the response schema and returned to the caller.
[!IMPORTANT]
With x-d2-event-rpc-call-type: CONVERSATION_BEGIN_END (the default) the HTTP request is held until the ESL procedure answers with CALL [_hTC] Result(...) ASYNC TC_E — also from its EXCEPTION_HANDLER. A procedure that does not answer leaves the caller waiting for its timeout.
Generating operations from D2000 metadata
A specification with a mapping block does not only publish what the file contains — SmartWeb opens a D2000 session, reads the RPC procedures of the ESL events and adds them to the document as further operations. The block also filters what is published:
- specificationFilePath: base.yaml
contextPath: discover
mapping:
allowedD2RpcEventNames: [ "E.*" ] # ESL events to publish
allowedD2RpcMethodNames: [ "*" ] # RPC procedures to publish
allowedD2RpcCallModes: [ REPLY, CONVERSATION_BEGIN_END ]The names are wildcard patterns, case-insensitive. specificationFilePath is optional here — without it the document is built purely from D2000 metadata. Changes in D2000 (a new RPC procedure, a changed structure definition) rebuild the document at runtime.
Values from HTTP headers and the response status code
Two extensions on components map payload properties onto the HTTP layer:
components:
x-property-in-mappings:
correlationId:
$parameterRef: "#/components/parameters/CorrelationIdHeader"
priority: PAYLOAD_THEN_PARAM # PAYLOAD_THEN_PARAM | PARAM_THEN_PAYLOAD | PARAM_ONLY
x-property-out-mappings:
result:
pattern: "^(?<httpStatusCode>[0-9]{3})?:?(?<value>.*)$"x-property-in-mappings— the named property may be filled from an HTTP header referenced through$parameterRef.prioritydecides what wins when both the payload and the header carry a value.x-property-out-mappings— a regular expression applied to a text output parameter. The named groupvaluebecomes the value written into the JSON, the named grouphttpStatusCodesets the HTTP status code of the response. This is how an ESL procedure answers with something other than200.
Nested structures
A D2000 structure is a flat table of typed columns — a cell cannot hold another structure. Arbitrarily deeply nested JSON is therefore decomposed relationally: the parent structure has a column holding the row's own key (rowId) and every nested structure has a column referencing the parent row's key (parentRowId). Each nested structure is passed as a separate RPC procedure parameter — the ESL procedure thus receives N flat structures and joins them through parentRowId == rowId.
Specification extensions
Attribute | Level | Meaning |
|---|---|---|
| operation ( | D2000 ESL Event hosting the RPC procedure, e.g. |
| operation ( | Optional ESL interface name for interface-implementing procedures |
| operation ( | RPC procedure name; defaults to the last path segment |
| operation ( |
|
| component schema, or an array-of-scalars property | Marks the component as a D2000 structure and names the target |
| structure schema | Name of the integer column holding the row's own key — the link target for nested structures |
| structure schema | Name of the integer column referencing the parent row's |
| property schema | Overrides the RPC parameter / column name derived from the JSON property name; used to resolve name collisions. On the |
Example
components:
schemas:
EdcSyncPayload: # envelope — its properties are the RPC parameters
type: object
properties:
subjects:
type: array
items: { $ref: "#/components/schemas/EdcSubject" }
EdcSubject:
type: object
x-d2-structure-name: "SD.EdcSubject"
x-d2-structure-rowid-column: "rowId" # transient integer key, generated by SmartWeb
properties:
id: { type: string }
temporalData:
type: array
x-d2-name: "subjectTemporalData" # RPC parameter name (JSON property stays temporalData)
items: { $ref: "#/components/schemas/EdcSubjectParametersTemporalDataItem" }
EdcSubjectParametersTemporalDataItem:
type: object
x-d2-structure-name: "SD.EdcSubjectParametersTemporalDataItem"
x-d2-structure-parent-rowid-column: "parentRowId"
properties:
id: { type: string }
name: { type: string }Arrays of scalars
An array whose elements are scalars has no matching D2000 value. Declaring x-d2-structure-name directly on the property maps such an array onto a flat SD.* structure with a single column — one row per element. The column name comes from x-d2-name on items, otherwise from the property name (x-d2-name on the property itself still names the RPC parameter). In the response such a structure is written back as an array of scalars. The same rules as for any other structure apply, so it can also be nested — it then needs x-d2-structure-parent-rowid-column.
subjects: # RPC parameter "subjects" -> SD.EdcSubjectIdList
x-d2-structure-name: "SD.EdcSubjectIdList"
type: array
items: { type: string, x-d2-name: "id" } # single column "id", one row per elementDates
Both format: date-time and format: date map onto a time column. A date-time value is read and written as a full UTC ISO-8601 timestamp, a date value as a plain yyyy-MM-dd day in the server's local time zone. A string property without either format stays a text column.
Requirements on the D2000 side
[!IMPORTANT]
The target SD.* structures must really exist in D2000 and their column names must match the names of the mapped properties (or x-d2-name). Cells are placed into the row by column name, so the property order in the specification does not have to match the column order of the structure and the link columns do not have to come first. Columns the specification does not map stay empty. If the structure does not exist, one of its mapped columns is missing, or a link column is not of type integer, the call fails with an error naming the structure and the column.
The link columns are transient — they are not JSON attributes and are not written to the response. The exception is a
rowIdmapped to an existing JSON attribute, which must betype: integerandrequired; the value is then taken from the request and is also written to the response. MappingparentRowIdto a JSON attribute is not allowed.Transient
rowIdvalues are generated by SmartWeb from a single counter per message, incrementally from1. ArowIdis unique within the whole message, not within one structure — the counter runs on across all parent structures, so two parents never carry the same id and aparentRowIdalways identifies exactly one row. An ESL procedure filling these columns for a response has to keep the same rule. ArowIdread from a payload attribute is not generated by SmartWeb, so keeping it unique is up to the caller.The order of the ESL procedure's formal parameters follows the specification: depth-first (pre-order) traversal, i.e. a structure immediately followed by its nested structures, recursively; all input parameters first, then the output ones. Within one schema the parameters follow the declaration order of its properties in the specification file. SmartWeb neither generates nor adapts the procedure signature — the specification is the source of truth.
[!IMPORTANT] SmartWeb passes the parameters positionally and does not verify the procedure — it reads nothing about the signature from D2000. What happens with a procedure declared in a different order is decided on the D2000 side. Whenever the specification changes, re-check the parameter order of every affected procedure.
Specification errors
The following cases are a hard error when loading the specification: the error is logged and the change does not take effect (the previously loaded working version stays deployed).
recursive structure schema (direct or indirect
$refcycle)RPC parameter name collision across the whole flattened tree, or column name collision inside one structure — resolve with the
x-d2-nameattributex-d2-structure-parent-rowid-columnmapped to an existing JSON propertyx-d2-structure-rowid-columnmapped to a property that is nottype: integeror notrequireda structure with nested structures but without
x-d2-structure-rowid-columna nested structure without
x-d2-structure-parent-rowid-columnx-d2-structure-nameon a property referencing a component — it belongs on the component itselfx-d2-structure-rowid-columnandx-d2-structure-parent-rowid-columnnaming the same column
Outbound OpenAPI connector
The outbound connector lets a D2000 ESL script call an external HTTP service. SmartWeb registers in D2000 as a process; the ESL script calls an RPC procedure on that process, the connector resolves it against the loaded OpenAPI specification, writes the RPC arguments as the JSON request body and sends the request. The answer is returned to D2000 as a second RPC call.
Both directions read the same x-d2-* extensions and the same nested-structure rules, so one specification file can serve the inbound API and the outbound connector at once.
Configuration
Every entry of openApiConnectors starts one independent connector — its own D2000 process, its own HTTP client, its own specifications.
smartweb:
application:
connectors:
openApiConnectors:
- local:
processName: "SELF.OPENAPI_CONNECTOR"
remote:
connectionUrl: "https://external-api.example.com/base/"
authType: "USERNAME_PASSWORD" # USERNAME_PASSWORD | API_KEY | OAUTH2
username: "user"
password: "secret"
# apiKey: "external-service-key" # authType: API_KEY
# realm: { ... } # authType: OAUTH2
truststore:
path: config/cert/truststore.p12
password: changeit
connectTimeout: 5000
requestTimeout: 45000
idleTimeout: 30000
addressResolutionTimeout: 5000
openApi:
enabled: true
specifications:
- specificationFilePath: external_api.yaml
specificationFormat: "YAML"
specificationVersion: "3.0"
generateDocumentation: trueProperty | Description |
|---|---|
| Name of the D2000 process the connector logs on as. The ESL script addresses this process — the object has to exist in D2000. |
| Base URL of the external service. The operation path from the specification is resolved against it. |
| Authentication of the outbound request — see the table below. Without it no authentication header is sent. |
| Truststore for verifying the TLS certificate of an |
| HTTP client timeouts in milliseconds (defaults 5000 / 45000 / 30000 / 5000). |
| Specifications of the external API — same properties as for the inbound direction; |
[!IMPORTANT]
connectionUrl has to end with a slash. The operation path is resolved against it as a relative URI, so https://host/base without the trailing slash loses the base segment and every call goes to https://host/….
Authentication types
| Header sent | Requires |
|---|---|---|
(not set) | none | — |
|
|
|
|
|
|
|
|
|
Calling it from an ESL script
INT _hConn
_hConn := %StrToHBJ("SELF.OPENAPI_CONNECTOR.DCP")
CALL [(0)] E.MyEvent.myOperation(_header, _rows, _nestedRows) ASYNC ON (_hConn) TC_BE _hTCThe connector is a JAPI process, so the call carries no object reference (
[(0)]) and the process is addressed by HOBJ (ON (_hConn)). D2000 registers the connector's session underlocal.processNameplus the.DCPextension — that is the object name%StrToHBJresolves; the bare process name is not an object and returns an invalid value. The object exists once the connector has logged on. An RPC procedure that implements an ESL interface cannot be called this way.The procedure name is
x-d2-event-name+.+x-d2-event-rpc-nameof the operation, written literally — D2000 object names may contain a full stop, so the name compiles. The connector splits it at the last full stop. An RPC name containing a hyphen cannot be written in ESL — use an underscore, the connector retries the lookup with hyphens.The arguments are positional, in the same flattened depth-first order as for the inbound direction, and all of them have to be passed, empty structures included.
The answer is not a return value. The connector answers on the conversation with
on<Rpc>Response(<output parameters>), or withon<Rpc>Error(errorCode, errorMessage). Both are declared in the calling script asRPC PROCEDURE [_hTC, TC_E]and close the conversation. If the procedure does not exist, the connector aborts the conversation instead.
RPC PROCEDURE [_hTC, TC_E] onMyOperationResponse(IN RECORD NOALIAS (SD.Header) _header)
; process the answer
END onMyOperationResponse
RPC PROCEDURE [_hTC, TC_E] onMyOperationError(IN TEXT _errorCode, IN TEXT _errorMessage)
LOGEX _errorCode + " - " + _errorMessage PRIORITY _LOG_PRTY_ERROR
END onMyOperationErrorError codes
errorCode is a mnemonic string, not an HTTP status code; some of them append the status of the remote answer.
| Cause |
|---|---|
| The |
| The RPC was called with more arguments than the operation has parameters |
| The JSON request body could not be built |
| Mapping the answer onto D2000 structures failed in SmartWeb — a missing column or link column |
| The external service answered with a non-2xx status |
| The answer could not be parsed |
| The request could not be sent (connection, DNS, timeout) |
| The connector's HTTP client is not running |
| An OAuth2 access token could not be obtained |
Generated client stubs
Generating the TypeScript client (structures.ts, events.ts) has moved out of OpenAPI — it is configured by the smartweb.application.generator.d2jsapi[] block and runs over the session of the core connector. The clientOutputDirectory and useLegacyApi keys of a specification were removed and leaving them in place has no effect. See Generating a TypeScript Client for D2000 RPC.
Generated specification documentation
For every processed specification SmartWeb builds a Markdown report and serves it on the same path the document itself is served on:
URL | Answers with |
|---|---|
| the deployed specification of that context |
| the report of every specification of the instance ( |
For example http://localhost:8098/smartweb/api/open/status.md.
[!NOTE]
There is one report URL per instance, not per context: a report belongs to a specification file, and
files do not map onto context paths — the specification of an outbound connector is published under none.
When the same file is used in both directions it appears once, with a section per direction. No file is written to disk — the report is rendered when it is requested and checked against D2000 through the session of that same request, i.e. with the rights of whoever asked for it (API key, JWT or Basic auth). It is switched off with generateDocumentation: false on that specification.
The report is short — it is a report, not a description of the specification:
one sentence per direction: YAML correctly processed with the number of bound operations, or YAML was NOT processed with the mapping error verbatim,
a
Structures on the D2000 sidesection — one section perSD.*the specification maps, whatever its state: it matches, it does not match (a missing column, a wrong type, arowId/parentRowIdthat is notInteger), it is missing in D2000, or D2000 has it but does not publish it (see below, with its HOBJ). Every one of them ends with the whole column layout the specification maps onto that structure, so it can be created or corrected from the report alone,an
RPC procedures the D2000 side has to declaresection — the ESL header of every bound RPC procedure (for the inbound direction including theCALL [_hTC] Result(…) ASYNC TC_Eanswer, for the outbound one the call and both callback procedures,on<Rpc>Response/on<Rpc>Error). These headers are generated from the specification; whether D2000 declares them is not checked — an RPC procedure lives in an ESL script. An output parameter that would repeat the name of an input one is renamed with anOutsuffix (_headerOut), because ESL does not allow the same parameter name twice — the parameters are passed positionally,a
Recursion in the specificationsection when the file contains a recursive schema.
When nothing is missing the whole report is a few lines long. The parameters of working operations and the columns of matching structures are not listed — that is the content of the specification itself, which openapi.yaml serves.
[!NOTE]
status.md answers even when the specification could not be processed at all — that report is the only place saying why.
Missing, or not published?
D2000 pushes structure definitions to a JAPI client as shared resources. An SD.* object can therefore
exist in D2000 and the session still not know its definition — typically for a freshly created structure that
no running D2000 side uses yet. SmartWeb therefore asks the kernel itself (getObjectInfo) whenever the
definition is missing, and the report distinguishes two states:
Definition in the session | Object in the kernel | Reported as |
|---|---|---|
present | — | the columns are checked |
absent | does not exist |
|
absent | exists | is not published by D2000, with its HOBJ and the advice to start / recompile the ESL script declaring it |
Only the structures are checked this way — the RPC procedures are not checked at all.
[!IMPORTANT]
As long as D2000 does not publish the definition of a structure, a call mapping it fails with 500 and the
same explanation — it is not a problem of the specification but of the state of the D2000 side.
The report is always current
It is rendered per request, so there is nothing to invalidate:
the specification files are checked on disk on every request and whatever changed is re-read — editing a file and reloading the URL is enough, no waiting for the file watch,
the D2000 side is read at request time — creating the missing
SD.*in CNF and reloading the URL is enough, no restart is needed,the RPC procedures are not checked — their headers are what the specification expects of the D2000 side, and they are listed whether or not D2000 declares them.
When the same specification is used in both directions (published as an inbound API and called through an outbound connector), the report has a section per direction and one shared Structures on the D2000 side section.
[!NOTE] Checking the structures against D2000 requires a D2000 session — the one of the request is used. When none can be resolved the report is served as well, but the structure check is marked as not validated, with the reason. No extra session is opened for the report.
Recursion in the specification
A component that references itself has no equivalent in D2000 — a structure is a flat table and a cell cannot hold another structure — so the whole specification is rejected. The report then carries a Recursion in the specification section: the cycle as a path (OrgNode -> children -> OrgNode) and its unrolling to a fixed depth — one component per level (OrgNodeL1 … OrgNodeL3), all mapped onto the same SD.*, the root declaring only x-d2-structure-rowid-column, every level below it also x-d2-structure-parent-rowid-column, and the last one carrying no nested property any more.