Skip to main content
Version: Next

Server Side Generator

The OpenAPI Generator for OpenEdge ABL/PASOE produces enterprise-ready server implementations with advanced features for PASOE deployment, including business logic frameworks, comprehensive validation, error handling, and configuration management.

Prerequisites

1. Setup OpenEdge Project in VSCode

  1. Create a new folder for your OpenEdge project (e.g., openapi_server)
  2. Open the folder in VSCode

Initialize Project Configuration

Use the API4UI Toolbox for easy project initialization (recommended):

  1. Right-click your project folder in VSCode Explorer
  2. Select API4UI Toolbox > ABL > Initialize > Initialize Project Configuration
  3. This creates the required openedge-project.json file for the Riverside extension

Alternatively, follow the VSCode ABL Github page for manual project configuration.

Configure Project

Configure your OpenEdge project according to your environment. The Riverside extension requires proper configuration for ABL language features to work correctly.

Note: Detailed configuration steps are beyond the scope of this guide. Refer to Riverside's documentation or use the API4UI Toolbox for guided setup.

2. Launch OpenAPI Generator Extension

  1. Open VSCode Command Palette (Ctrl + Shift + P / Cmd + Shift + P)
  2. Type and select: "Launch OpenAPI generator 😎 (vscode sync)"

Configure Generation Settings

  1. Choose Generation Type: Select Server from the dropdown
  2. Choose Language: Select abl-pasoe
  3. Configure Settings: Click the Settings button to open the configuration panel

General Settings:

  • packageName: Set your desired package name (default: "OpenAPI" if not specified)

PASOE Environment:

  • pasoeDlcPath: Path to your OpenEdge DLC installation
  • pasoeWorkDir: Working directory for PASOE instances
  • projectPath: Path to your current project folder

PASOE Instance:

  • pasoeInstanceName: Name for your PASOE instance (leave default if creating new, or specify existing)

Note: These settings will be saved to .vscode/settings.json and should be committed to version control for team consistency.

Select OpenAPI Specification

  1. Click Browse next to "Select file"
  2. Choose your OpenAPI specification file (.yaml or .json)

Generate Code

  1. Ensure Project Path is set to your current project folder
  2. Click Sync to Selected Location

The generated code will be placed directly in your project folder under the specified package name.

Example Specs: Download this example spec to test the generator

GIF: VSCode OpenAPI Generator Extension 0.7.0 (Server)

3. Post-Generation Setup

Load Saved Settings

When reopening the extension later:

  1. Click Load Settings at the top
  2. Select from previously saved configurations
  3. Settings will be restored automatically

GIF: VSCode OpenAPI Generator Extension 0.7.0 (Load Settings)

Deploy with PASOE Scripts

After generation, setup scripts are available in [package]/Resources/:

Linux:

./setup-pasoe-linux.sh

Windows:

.\setup-pasoe-windows.ps1

These scripts handle PASOE instance creation, configuration, and deployment automatically. You don't need a pre-existing PASOE instance - the scripts will create one for you.

Note: If you need to modify script variables after generation, edit them directly in the script files. They should be pre-configured based on your extension settings.

4. Project Structure

The generated PASOE server follows this structure:

[package]/
├── BusinessLogic/
│ ├── Abstract*BusinessLogic.cls # Abstract base classes
│ └── *BusinessLogic.cls # Concrete implementations
├── Config/ # Config Manager
├── Factories/ # Business logic factories
├── Helpers/ # Helpers used by the generated code
├── Models/
│ ├── ApiRequests/ # Request models
│ └── Builders/ # Builder classes for request models
│ ├── ApiResults/ # Response models
│ ├── Builders/ # Model builders
│ ├── Collections/ # Collection models (e.g. StringList)
│ └── Exceptions/ # Custom exceptions
│ └── Builders/ # Builder classes for exceptions
├── Resources/ # PASOE deployment resources
├── ServiceAdapters/ # PASOE service adapters
└── logging.config # Logging configuration

5. Configuration Management

The server supports dynamic configuration of business logic factories, variant selection, and validation settings via a JSON configuration file.

JSON Configuration File Setup

Initialization: Call [package].Config.ConfigManager:Initialize() during session startup (see sessionStartup.p). Config example generated in Resources/[package]-api-config.json. Copy to anywhere in PROPATH for search() to find it.

Hot Reloading: Configuration changes picked up without restart (use setup-script-* action refresh-agents for dynamic apply).

In-Code Configuration

For scenarios where you prefer to configure the server entirely in code without relying on config files, you can use the ConfigModelBuilder and pass the configuration directly to ConfigManager:Initialize(). This mirrors the JSON structure above, allowing programmatic configuration equivalent to the file-based approach:

using [package].Config.ConfigManager.
using [package].Config.ConfigModelBuilder.
using [package].Config.SerializationOptionsBuilder.
using [package].Models.Collections.StringMap.

var ConfigModelBuilder oBuilder.
var StringMap oFactories.

// Create builder and configure step-by-step
oBuilder = new ConfigModelBuilder()
:SetStrictValidation(true)
:SetReloadEnabled(false) // Disable file monitoring
:SetFileCheckInterval(300).

// Add business logic factories
oBuilder:AddBusinessLogicFactory("default", "[package].Factories.BusinessLogicFactory")
oBuilder:AddBusinessLogicFactory("mock", "[package].Factories.MockBusinessLogicFactory").

// Configure variant selection
oBuilder:SetVariantSelectionEnabled(false)
oBuilder:SetVariantSelectionHeaderName("X-API-Variant").

// Configure serialization options
oBuilder:SetSerializationOptions(new SerializationOptionsBuilder()
:SetEmitDefaults(true)
:SetOmitEmptyCollections(false)
:SetOmitNulls(false)).

// Build and initialize
[package].Config.ConfigManager:Initialize(oBuilder:Build()).

Alternative simplified approach:

using [package].Config.ConfigManager.
using [package].Config.ConfigModelBuilder.

var ConfigModelBuilder oBuilder.

// Use WithDefaults() to set all OpenAPI spec defaults, then override as needed
oBuilder = new ConfigModelBuilder()
:WithDefaults() // Sets conservative defaults
:SetStrictValidation(true)
:SetReloadEnabled(false).

[package].Config.ConfigManager:Initialize(oBuilder:Build()).

This approach:

  • Eliminates the need for config files in the PROPATH
  • Provides full programmatic control over configuration
  • Skips file system checks for better performance
  • Still allows optional file monitoring if ReloadEnabled is set to true

Configuration Schema

The Orders-api-config.json file follows this JSON Schema (all fields optional with fallbacks):

{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "[package] API Configuration",
"description": "Configuration for ConfigManager, factories, variants, and serialization. All fields optional with fallbacks.",
"type": "object",
"properties": {
"strictValidation": {
"type": "boolean",
"description": "Toggle strict validation in abstract BusinessLogic (e.g., stricter checks on required fields). Falls back to false.",
"default": false
},
"serialization": {
"type": "object",
"description": "Controls JSON serialization behavior in models (ToJson() method).",
"properties": {
"emitDefaults": {
"type": "boolean",
"description": "Include default values in JSON output when not explicitly set. Defaults to false.",
"default": false
},
"omitEmptyCollections": {
"type": "boolean",
"description": "Omit empty arrays/maps from JSON output. Defaults to false.",
"default": false
},
"omitNulls": {
"type": "boolean",
"description": "Omit null values from JSON output. Defaults to false.",
"default": false
}
},
"default": {
"emitDefaults": false,
"omitEmptyCollections": false,
"omitNulls": false
}
},
"reloadEnabled": {
"type": "boolean",
"description": "Enable hot-reloading of config file changes without restart. Falls back to true.",
"default": true
},
"fileCheckInterval": {
"type": "integer",
"description": "Interval in seconds to check for config file changes. Falls back to 300.",
"default": 300
},
"businessLogicFactories": {
"type": "object",
"description": "Map of variant keys to full ABL class paths for BusinessLogic factories. Omitting uses hardcoded defaults.",
"additionalProperties": {
"type": "string",
"description": "Full class path, e.g., 'Orders.Factories.BusinessLogicFactory'"
},
"default": {
"default": "Orders.Factories.BusinessLogicFactory",
"mock": "Orders.Factories.MockBusinessLogicFactory"
}
},
"defaultBusinessLogicFactoryKey": {
"type": "string",
"description": "Default variant key from businessLogicFactories. Falls back to 'default'.",
"default": "default"
},
"variantSelection": {
"type": "object",
"description": "Config for HTTP header-based BusinessLogic switching. Disabled if omitted.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable variant switching via header. Falls back to false.",
"default": false
},
"headerName": {
"type": "string",
"description": "HTTP header name for variant key (e.g., 'X-API-Variant').",
"default": "X-API-Variant"
}
},
"default": {
"enabled": false,
"headerName": "X-API-Variant"
}
}
},
"additionalProperties": false,
"version": "1.0"
}

Example Configuration:

{
"strictValidation": false,
"defaultBusinessLogicFactoryKey": "default",
"businessLogicFactories": {
"default": "[package].Factories.BusinessLogicFactory",
"mock": "[package].Factories.MockBusinessLogicFactory"
},
"variantSelection": {
"enabled": false,
"headerName": "X-API-Variant"
},
"serialization": {
"emitDefaults": true,
"omitEmptyCollections": false,
"omitNulls": false
},
"reloadEnabled": true,
"fileCheckInterval": 600
}

Factory Registration

The ConfigManager loads factory classes from the businessLogicFactories map and registers them in the FactoryRegistry.

Default Factory:

  • Key: "default"
  • Class: Orders.Factories.BusinessLogicFactory - Production BusinessLogic implementation

Mock Factory:

  • Key: "mock"
  • Class: Orders.Factories.MockBusinessLogicFactory - Test data (enable via generator option mockData)

Custom Factories: Add via config for specialized implementations (implement IBusinessLogicFactory with methods returning Abstract*BusinessLogic instances). Example:

"businessLogicFactories": {
"optimized": "Orders.Factories.OptimizedBusinessLogicFactory"
}

Registry uses dynamic-new to instantiate factories; fallback to "default" with logging if invalid.

Variant Selection

Enable with "variantSelection": {"enabled": true} in config. Use HTTP header (default: "X-API-Variant") to select BusinessLogic variant per request (fallback to default with logging if invalid):

curl -H "X-API-Variant: mock" /api/orders  # Uses mock BusinessLogic
curl /api/orders # Uses default BusinessLogic

Serialization Options

The serialization object controls how models serialize to JSON via the ToJson() method:

  • emitDefaults (boolean, default: false): Include default values in JSON output when not explicitly set.
  • omitEmptyCollections (boolean, default: false): Omit empty arrays/maps from JSON output. When true, empty collections are excluded from serialization.
  • omitNulls (boolean, default: false): Omit null values from JSON output. When true, properties with null/unknown values are excluded.

Hot Reloading:

  • reloadEnabled (boolean, default: true): Enable hot-reloading of config file changes without restarting the application. When enabled, the system automatically detects and applies configuration changes.
  • fileCheckInterval (integer, default: 300): Interval in seconds to check for config file changes when hot reloading is enabled. Lower values provide faster config updates but increase file system polling.

IncludeAll Logic: When emitDefaults: true AND omitEmptyCollections: false AND omitNulls: false, models use "include all" mode, serializing every property in template order. Otherwise, models use "partial" mode, serializing only properties explicitly set via builders (in builder call order).

Property Order: Partial serialization follows builder method call order (user-controllable). Full serialization follows OpenAPI spec property order. Explicitly set ? values serialize as null in JSON (Set{property}(?)).

Strict Validation

Set "strictValidation": true for additional validation checks in abstract BusinessLogic classes. When enabled, it performs stricter validation on required fields (e.g., checking for empty strings on required fields).

Important Notes:

  • Production: Keep strictValidation: true for robust validation
  • Development/Testing: Set to false if you need more lenient validation during development
  • Invalid data types: Will always throw exceptions regardless of this setting
  • Behavior: When strictValidation: false, validation issues are logged as warnings but don't automatically throw exceptions. Check HasErrors() in your concrete BusinessLogic classes to handle validation errors appropriately.

6. Business Logic Implementation

Inheritance Pattern

The generated server uses an inheritance pattern where you implement concrete business logic by extending abstract base classes:

using Petstore.BusinessLogic.AbstractPetBusinessLogic.

class Business.MyApp.PetBusinessLogic inherits AbstractPetBusinessLogic:

constructor public PetBusinessLogic():
logger = LoggerBuilder:GetLogger("Business.MyApp.PetBusinessLogic").
end constructor.

method public override AddPetResult AddPet(input poPayload as AddPetRequest):
var AddPetResult oResult = new AddPetResult().

// Your business logic here
// Access request data via poPayload
// Return appropriate result

return oResult.
end method.
end class.

Validation Hooks

The framework provides validation hooks that allow you to add custom validation logic:

class Business.MyApp.PetBusinessLogic inherits AbstractPetBusinessLogic:

method public override ValidationErrorDetails ValidateSchemaAddPet(input poPayload as AddPetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Custom schema validation beyond OpenAPI spec
if poPayload:Pet:PhotoUrls:Count = 0
then oErrors:AddFieldError("pet.photoUrls", "required", "At least one photo URL required").

return oErrors.
end method.

method public override ValidationErrorDetails ValidateBusinessRulesAddPet(input poPayload as AddPetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Business rule validation
if poPayload:Pet:Name = "Fluffy"
then oErrors:AddFieldError("pet.name", "business_rule", "Sorry, no pets named Fluffy allowed").

return oErrors.
end method.
end class.

Error Handling

The framework provides streamlined error handling with RFC 7807 compliant responses:

method public override AddPetResult AddPet(input poPayload as AddPetRequest):
var AddPetResult oResult = new AddPetResult().

// Business validation
if poPayload:Pet:Name = ?
then throw oResult:BadRequestException("Pet name is required").

// Business logic
if not CanAddPet(poPayload:Pet)
then throw oResult:ConflictException("Pet already exists").

// Success
oResult:SetCreated(oCreatedPet).

return oResult.
end method.

7. Builder Pattern

The server-side code uses the same builder pattern as the client for creating complex model objects.

Simple Models

using Petstore.Models.Pet.
using Petstore.Models.Builders.PetBuilder.

var Pet oPet = new PetBuilder()
:SetName("Fluffy")
:SetId(123)
:SetStatus("available")
:Build().

Complex Models with Nested Objects

using Petstore.Models.Pet.
using Petstore.Models.Builders.PetBuilder.
using Petstore.Models.Builders.CategoryBuilder.

var Pet oPet = new PetBuilder()
:SetName("Fluffy")
:SetId(123)
:SetCategory(new CategoryBuilder()
:SetName("Dogs")
:SetId(1)
)
:SetStatus("available")
:Build().

Models with Lists

using Petstore.Models.Pet.
using Petstore.Models.Builders.PetBuilder.
using Petstore.Models.Builders.TagBuilder.

var Pet oPet = new PetBuilder()
:SetName("Fluffy")
:SetId(123)
:AddTag(new TagBuilder()
:SetName("friendly")
:SetId(1)
)
:AddTag(new TagBuilder()
:SetName("playful")
:SetId(2)
)
:AddPhotoUrlsString("http://example.com/photo1.jpg")
:AddPhotoUrlsString("http://example.com/photo2.jpg")
:SetStatus("available")
:Build().

Model Builders with Default Values

Model builders include additional methods to set default values defined in your OpenAPI specification:

  • WithDefaults() will set all defaults defined in the spec
  • WithDefault{propertyName}() will set only the specific default for that property
using Petstore.Models.Pet.
using Petstore.Models.Builders.PetBuilder.
using Petstore.Models.Builders.TagBuilder.

var Pet oPet = new PetBuilder()
:WithDefaults()
:SetName("Fluffy")
:AddTag(new TagBuilder()
:SetName("friendly")
:SetId(1)
)
:AddTag(new TagBuilder()
:SetName("playful")
:SetId(2)
)
:Build().

Notes:

  • You can override specific defaults by calling setters after WithDefaults() (calling order matters)
  • At this moment, only defaults for primitive data types are handled. This limitation will be addressed in a later version.

8. API Request/Response Handling

Request Processing

Request objects contain all the data from the incoming API call:

method public override AddPetResult AddPet(input poPayload as AddPetRequest):
var AddPetResult oResult = new AddPetResult().

// Access path parameters
var int64 iPetId = poPayload:PetId.

// Access query parameters
var character cStatus = poPayload:Status.

// Access request body
var Pet oPet = poPayload:Body.

// Access headers
var character cApiKey = poPayload:ApiKey.

return oResult.
end method.

Response Building

Result objects provide methods for different HTTP responses:

method public override GetPetByIdResult GetPetById(input poPayload as GetPetByIdRequest):
var GetPetByIdResult oResult = new GetPetByIdResult().

// Find the pet
var Pet oPet = FindPetById(poPayload:PetId).

if not valid-object(oPet)
then throw oResult:NotFoundException(substitute("Pet with ID &1 not found", poPayload:PetId)).

// Return successful response
oResult:SetOk(oPet). // Sets 200 status with pet data

return oResult.
end method.

9. Logging Configuration

Server-Specific Logging

The server logging configuration integrates with PASOE's logging system:

{
"logger": {
"[package]": {
"logLevel": "DEBUG",
"filters": [
"ERROR_FORMAT",
"FULL_TEXT_FORMAT",
{
"name": "NAMED_FILE_WRITER",
"fileName": "${env.catalina.base}/logs/[package]_${t.today}.log",
"appendTo": true
}
]
}
}
}

Multiple Project Configuration

Configure separate logging for different components and projects:

{
"logger": {
"[package]": {
"logLevel": "INFO",
"filters": [
{
"name": "NAMED_FILE_WRITER",
"fileName": "${env.catalina.base}/logs/[package]_${t.today}.log",
"appendTo": true
}
]
},
"[package].BusinessLogic": {
"logLevel": "DEBUG",
"filters": [
{
"name": "NAMED_FILE_WRITER",
"fileName": "${env.catalina.base}/logs/[package]-business_${t.today}.log",
"appendTo": true
}
]
},
"[package].ServiceAdapters": {
"logLevel": "WARN",
"filters": [
{
"name": "NAMED_FILE_WRITER",
"fileName": "${env.catalina.base}/logs/[package]-adapters_${t.today}.log",
"appendTo": true
}
]
}
}
}

PASOE Log Integration

The ${env.CATALINA_BASE} variable ensures logs are written to PASOE's standard log directory alongside other server logs.

10. Error Handling & Validation

RFC 7807 Compliance

All errors follow RFC 7807 (Problem Details) format:

{
"type": "https://api.example.com/errors/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "The request contains invalid data",
"instance": "/operations/addPet"
}

Custom Error Models

Define custom error models in your OpenAPI spec for structured error responses:

// OpenAPI spec defines CustomError model
// Generated classes handle serialization automatically

method public override AddPetResult AddPet(input poPayload as AddPetRequest):
var AddPetResult oResult = new AddPetResult().

if poPayload:Pet:Age < 0
then do:
undo, throw new CustomErrorException(new CustomErrorBuilder()
:SetErrorCode("INVALID_AGE")
:SetMessage("Pet age cannot be negative")
:SetDetails(/* additional data */)
:Build()
).
end.

return oResult.
end method.

Validation Patterns

Use the ValidationContext pattern for optimized validation:

method public override ValidationContext ValidateAddPetContext(input poPayload as AddPetRequest):
var ValidationContext oContext = new ValidationContext("AddPet").
var ValidationErrorDetails oSchemaErrors, oBusinessErrors.

// Schema validation
oSchemaErrors = this-object:ValidateSchemaAddPet(poPayload).

if valid-object(oSchemaErrors) and oSchemaErrors:HasErrors()
then do:
oContext:MergeErrors(oSchemaErrors).
oContext:SetSchemaValidationFailed().
end.

// Business rules validation (only if schema passed)
if oContext:IsValid()
then do:
oBusinessErrors = this-object:ValidateBusinessRulesAddPet(poPayload).
if valid-object(oBusinessErrors) and oBusinessErrors:HasErrors()
then do:
oContext:MergeErrors(oBusinessErrors).
oContext:SetBusinessRuleValidationFailed().
end.
end.

return oContext.
end method.

Note: For comprehensive validation guidance, see Error Handling & Validation Guide

11. PASOE Deployment

The generated code includes comprehensive deployment automation with support for both legacy and modern PASOE configurations. The deployment scripts provide intelligent port conflict avoidance, automatic handler ordering, and seamless format conversion between legacy and JSON configurations. For detailed deployment instructions, see PASOE Deployment Guide.

Setup Scripts

The generated code includes automated setup scripts:

  • setup-pasoe-linux.sh - Linux deployment script
  • setup-pasoe-windows.ps1 - Windows deployment script
  • update-handlers.sh - Handler configuration script

Handler Configuration

The system supports both legacy and modern handler configuration formats:

JSON Format (Modern - OpenEdge 12.2+ with migration)

The ROOT.handlers file maps URIs to PASOE handler classes:

{
"version": "2.0",
"serviceName": "ROOT",
"handlers": [
{
"uri": "/pet/{petId}",
"class": "Petstore.ServiceAdapters.PetServiceAdapter",
"enabled": true
}
]
}

Legacy Format (oeprop.properties)

For legacy systems, you can provide handlers in oeprop.properties format. The system supports two variations:

Full Path Format:

+{AblApp}.{WebApp}.WEB.handler1=Petstore.ServiceAdapters.PetServiceAdapter: /pet/{petId}
+{AblApp}.{WebApp}.WEB.handler2=Petstore.ServiceAdapters.StoreServiceAdapter: /store/order

Simple Format (handler number only):

handler1=Petstore.ServiceAdapters.PetServiceAdapter: /pet/{petId}
handler2=Petstore.ServiceAdapters.StoreServiceAdapter: /store/order

Automatic Format Conversion: The deployment scripts automatically detect and convert between formats. You can provide either format - the system will handle the conversion appropriately for your PASOE version.

Deployment Automation

Use the provided scripts for automated deployment:

# Linux - Full setup
./setup-pasoe-linux.sh

# Linux - Specific action (e.g., refresh agents)
./setup-pasoe-linux.sh refresh-agents

# Windows PowerShell - Full setup
.\setup-pasoe-windows.ps1

# Windows PowerShell - Specific action (e.g., refresh agents)
.\setup-pasoe-windows.ps1 -Action refresh-agents

Setting Default Actions: For development convenience, you can set default actions by editing the script variables. For example, to make the script refresh agents by default during development:

Linux (setup-pasoe-linux.sh):

# Near the top of the script, change:
ACTION="full"
# To:
ACTION="refresh-agents"

Windows (setup-pasoe-windows.ps1):

# Near the top of the script, change:
[string]$Action = "full"
# To:
[string]$Action = "refresh-agents"

Then simply run ./setup-pasoe-linux.sh or .\setup-pasoe-windows.ps1 without arguments to perform your default action.

These scripts handle:

  • PROPATH configuration
  • Handler registration and format conversion
  • Service startup
  • Basic validation

For detailed usage instructions and advanced options, see PASOE Deployment Guide.

12. Working Demo in Postman

Instantly you have a working demo in Postman.

  1. Drag & drop your OpenAPI spec in Postman
  2. Change in the collection the variable baseUrl to http://localhost:28000/{webAppName}/web
  3. Run request postman-openapi-server