Introduction
For our UI, each beverage must include specific data to render properly on screen. The required data for each beverage is:
- Name: The display name of the beverage.
- Icon: The URL of an icon representing the beverage.
- Primary Color: The main color associated with the beverage.
- Secondary Color: (Optional) Used when beverages are mixed with multiple flavors to depict a blended experience.
- Availability: Indicates whether the beverage is available for pouring, as determined by the Beverage Graph.
Of these, only the availability is managed by the backend. All additional display data (name, icon, colors) must come from another data source.
Data Layers in a Consumer UI
A common pattern in building a Consumer UI is to separate brandset data into two distinct layers:
Recipe Data:
- Used by the Java backend to construct the Beverage Graph.
- Determines availability and visibility of beverages.
UI Data:
- Specific to the application’s user experience.
- Includes icons, colors, layout specifications, and other UI-only details.
These layers are maintained separately and are linked by beverage IDs. This approach allows the Java application to remain stable, even when different UI applications require radically different experiences. If the UI needs additional data in the future, only the UI data layer must change—leaving the backend and Beverage Graph intact.
The Dispense SDK supports this decoupling by enabling base model data to be extended dynamically via an extension framework. In this tutorial, we focus on two primary extension points used to augment the Availability Model, which reflects the calculated Beverage Graph.
Data Sources
Availability Data
When the Dispense SDK’s Availability Model loads, it initially returns a small set of attributes. For example, a response might look like this:
{
"status": 200,
"version": {
"major": 1,
"minor": 0
},
"data": {
"rebuild": false,
"groups": {
"beverages": [
{
"id": "1",
"altId": "1",
"visible": true,
"note": "Water",
"available": true
},
...
]
}
}
}UI Data
Separately, UI-specific data is maintained (typically within the Java system application’s brandset or a separate artifact). This file holds data like the display name, icon, and colors for each beverage. An example UI schema might be:
{
"beverages": [
{
"id": "1",
"name": "Water",
"icon": "/system/brandset/icons/water-drop.svg",
"colors": {
"primary": "#45B1FF",
"text": "#027BA8"
}
},
...
],
"flavors": []
}
Integrating Additional Data into the Availability Model
The goal is to integrate the UI data into the Availability Model so that the complete beverage data is available throughout the application without additional lookups in the UI code.
This integration is achieved using extension points in the kOS Dispense SDK, which support:
Availability Loaders:
Mechanisms to load and parse external data (such as the ui.json file) into a usable context.
Availability Mappers:
Functions that map the loaded UI data to each Availability Model, enriching the model with additional properties.
Extension Points
Availability Loader
When a Nozzle initializes, it loads the current availability state and creates Availability Models. As part of this process, the framework checks for any registered Availability Loaders.
For example, the following code snippet shows an Availability Loader that fetches UI data from a ui.json file, then normalizes it into a context for mapping:
// libs/training-zero4-models/src/extension/availability/loader.ts
import { KosLog } from '@kosdev-code/kos-ui-sdk';
import type {
BrandsetBeverageData,
BrandsetUIData,
} from '../../types/brandset';
const BEVERAGE_UI_DATA_URL = 'http://localhost:8081/system/brandset/ui.json';
export interface BeverageContext {
beverages: Record<string, BrandsetBeverageData>;
}
const DEFAULT_CONTEXT: BeverageContext = {
beverages: {},
};
/**
* Loads the beverage context from the brandset ui.json file.
* @returns {Promise<BeverageContext>} The beverage context that can be used in the
* mapper
*/
export const beverageContextLoader = async (): Promise<BeverageContext> => {
// fetch the ui.json file from the brandset
const response = await fetch(BEVERAGE_UI_DATA_URL);
if (response.status !== 200) {
KosLog.error(
'Failed to load beverage context. Returning the default context.'
);
return DEFAULT_CONTEXT;
}
const json: BrandsetUIData | null = await response.json();
// normalize the data into a map of beverage data to make
// it easier for random access in the mapping
const beverages =
json?.beverages?.reduce((acc, beverage) => {
acc[beverage.id] = beverage;
return acc;
}, {} as Record<string, BrandsetBeverageData>) || {};
return { beverages };
};Availability Mapper
Next, we define a TypeScript interface for the additional UI data to be added to the Availability Model:
import { AvailabilityModel } from '@kosdev-code/kos-dispense-sdk';
/**
* Represents the beverage data as it appears in the Availability Model after mapping.
*/
export interface TrainingBeverage {
name: string; // Display name of the beverage.
icon: string; // Fully qualified URL for the beverage icon.
colors: {
primary: string;
secondary?: string;
text?: string;
};
}
The mapper is then executed for each Availability node. It takes the raw availability data and the context from the loader, then returns a TrainingBeverage object with the additional UI data:
// libs/training-zero4-models/src/extension/availability/mapper.ts
import type { NozzleServices } from '@kosdev-code/kos-dispense-sdk';
import type { BrandsetBeverageData } from '../../types/brandset';
import type { TrainingBeverage } from '../../types/training-zero4-models';
import type { BeverageContext } from './loader';
const extractIconUrl = (beverageData?: BrandsetBeverageData): string => {
if (!beverageData) {
return '';
}
return encodeURI(`http://localhost:8081${beverageData.icon}`);
};
const extractBeverageName = (beverageData?: BrandsetBeverageData): string | null => {
if (!beverageData) {
return null;
}
return beverageData.name;
};
const extractColors = (beverageData?: BrandsetBeverageData) => {
if (!beverageData) {
return { primary: 'lightblue', text: 'black' };
}
return beverageData.colors;
};
export const trainingBeverageDataMapper = async (
data: NozzleServices.BeverageResponse,
context: BeverageContext
): Promise<TrainingBeverage> => {
const id = data.id;
const beverageData = context.beverages[id];
const iconUrl = extractIconUrl(beverageData);
const name = extractBeverageName(beverageData) ?? data.note ?? 'Unknown';
const colors = extractColors(beverageData);
return {
name,
colors,
icon: iconUrl,
};
};Utility Types
To simplify type references in the CUI Kit, you can define utility types. For example, rather than repeatedly writing AvailabilityModel<TrainingBeverage>, define a utility type:
// Utility type for beverage data in the Availability Model.
export type BeverageModel = AvailabilityModel<TrainingBeverage>;Registering the Extension Points
Finally, register the extension points with the DispenseExtensionManager to ensure they are executed during the model lifecycle:
import {
beverageContextLoader,
pourableRequestMapper,
trainingBeverageDataMapper,
} from '@training-zero4/training-zero4-models';
KosModelRegistry.dispense.models();
const dispenseExtensionManager = new DispenseExtensionManager();
// Optionally register the pourable data mapper (redundant if using the default representation).
dispenseExtensionManager.registerSelectedPourableDataMapper(pourableRequestMapper);
// Register the context loader for Availability models.
dispenseExtensionManager.registerAvailabilityLoader(beverageContextLoader);
// Register the data mapper for Availability models.
dispenseExtensionManager.registerAvailabilityDataMapper(trainingBeverageDataMapper);Explanation:
- The DispenseExtensionManager enables registration of custom functionality at defined extension points, allowing the SDK to be augmented without needing a new build.
- When the NozzleModel loads availability data, it executes any registered Availability Loaders (like beverageContextLoader), creating a context.
- The registered data mapper (trainingBeverageDataMapper) is then applied to each Availability node, enriching the model’s data attribute with the additional UI information.
Through these extension points, the Dispense SDK seamlessly integrates application-specific UI data into the Availability Models, supporting a decoupled and flexible development experience between the frontend and backend.
