# Config tables

Validate incoming codes and translate between the code sets of external systems.

A config table is an `.ini` file in `config/user-conf` that holds either the list of codes a service accepts or the mapping from an external system's codes to that list. Config tables are [configuration files](https://zato.io/docs/dev/examples/config.html) and a service reads them through `self.config`. A change to a file is applied on all servers without a restart, and the files are [editable in the dashboard](https://zato.io/docs/dev/config/dashboard.html).

A config table provides two methods. `validate` returns whether a value is in the accepted list. `translate` returns the accepted code that an external system's code corresponds to, or, when a target system is given, the code that the target system uses for the same value.

## Code lists and mapping tables {#code-lists-and-mapping-tables}

A file with a section named `[codes]` is a code list. Each key is an accepted code and each value is its description:

```ini
# config/user-conf/statuses.ini

[codes]
ACTIVE      = Active and billable
SUSPENDED   = Suspended by billing
DEACTIVATED = Deactivated, number released
```

A file without a `[codes]` section is a mapping table. Each section is named after an external system, each key is a code that system sends and each value is the accepted code it corresponds to:

```ini
# config/user-conf/partners.ini

[NORDIC_MOBILE]
ACTIVE = ACTIVE
BARRED = SUSPENDED
CLOSED = DEACTIVATED

[ALPINE_TELECOM]
IN_SERVICE  = ACTIVE
NON_PAYMENT = SUSPENDED
CANCELLED   = DEACTIVATED
```

`[codes]` is the only reserved section name. Every other section name identifies an external system.

## Validating a value {#validating-a-value}

`validate` returns `True` when the value is a key of the `[codes]` section and `False` otherwise:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class UpdateSubscriberStatus(Service):

    def handle(self):

        status = self.request.payload['status']

        if not self.config.statuses.validate(status):
            self.logger.warning('Status `%s` is not an accepted value', status)
            return

        description = self.config.statuses.codes[status]
        self.logger.info('Status `%s` accepted: %s', status, description)
```

The `[codes]` section is a configuration section like any other, so the description of a code is read directly from `self.config.statuses.codes`.

## Translating a value {#translating-a-value}

`translate` takes the external system the value came from and the value itself, and returns the accepted code:

```python
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT')
```

1. `self.config.partners` is the mapping table to read.
2. `source` is the section of that table, which identifies the external system.
3. `code` is the key to look up in that section.

The call returns `SUSPENDED`.

The `codes` parameter names a code list that the result is checked against before it is returned. With `codes='statuses'`, a value that the mapping table produces but that `statuses.ini` does not list is not returned:

```python
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT', codes='statuses')
```

`translate` returns `None` in three cases:

1. The mapping table has no section named in `source`.
2. The section has no key named in `code`.
3. `codes` is given and the translated value is not in that code list.

A result of `None` is not an error. How to handle it is up to the service's own code.

## Translating to a target system {#translating-to-a-target-system}

The `target` parameter names a second section of the same mapping table. The value is looked up among the values of that section and the key that holds it is returned:

```python
status = self.config.partners.translate(source='ALPINE_TELECOM', code='NON_PAYMENT', target='NORDIC_MOBILE')
```

1. `NON_PAYMENT` is looked up in `[ALPINE_TELECOM]` and resolves to `SUSPENDED`.
2. `SUSPENDED` is looked up among the values of `[NORDIC_MOBILE]`.
3. The key that holds it, `BARRED`, is returned.

Both systems are sections of one file, so each mapping is configured once. The section that is read forwards for one system is read backwards for another.

`translate` returns `None` when the mapping table has no section named in `target` and when no key of that section holds the value, in addition to the three cases listed above.

## A value under more than one target code {#a-value-under-more-than-one-target-code}

A target section may hold the same value under more than one key:

```ini
[NORDIC_MOBILE]
ACTIVE = ACTIVE
BARRED = SUSPENDED
FROZEN = SUSPENDED
CLOSED = DEACTIVATED
```

`SUSPENDED` is held under both `BARRED` and `FROZEN`. There is no single code to return, so `translate` raises `AmbiguousTarget`:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.common.user_config import AmbiguousTarget
from zato.server.service import Service

class ForwardSubscriberStatus(Service):

    def handle(self):

        try:
            status = self.config.partners.translate(
                source='ALPINE_TELECOM', code='NON_PAYMENT', target='NORDIC_MOBILE')
        except AmbiguousTarget as e:
            self.logger.warning('Cannot translate: %s', e)
            return

        self.logger.info('Target status is `%s`', status)
```

The exception message names the file, the target section, the value and the keys that hold it:

error

```text
partners.ini, target NORDIC_MOBILE, value SUSPENDED maps to 2 keys: BARRED, FROZEN
```

There are two resolutions. The target section is changed to hold the value under a single key, or the service reads `self.config.partners.NORDIC_MOBILE` directly and selects the key itself. The dashboard shows every key a value is held under, so the duplicate is visible while the file is edited.

## Processing a status update end to end {#processing-a-status-update-end-to-end}

The service below receives a subscriber status from a roaming partner, translates it to an accepted status from `statuses.ini` and forwards the result to a billing system over a REST connection. The billing system receives accepted codes only:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class SyncSubscriberStatus(Service):

    def handle(self):

        partner = self.request.payload['partner_id']
        subscriber_id = self.request.payload['subscriber_id']
        partner_status = self.request.payload['status']

        status = self.config.partners.translate(source=partner, code=partner_status, codes='statuses')

        if status is None:
            self.logger.warning('No mapping for code `%s` from source `%s`', partner_status, partner)
            self.response.payload = {'is_accepted': False, 'received_status': partner_status}
            return

        conn = self.rest['Billing API']
        _ = conn.post({'subscriber_id': subscriber_id, 'status': status})

        self.response.payload = {'is_accepted': True, 'status': status}
```

The incoming value is kept in `partner_status` and the translated value in `status`, so the original value is available for the log message and the response payload.

A partner that renames its states, or a new partner, is a change to `partners.ini` and to no service.

## Healthcare - LOINC observation codes {#healthcare-loinc-observation-codes}

A measurement in an HL7 v2 message is identified by whatever observation code the sending application uses. The accepted vocabulary is LOINC, so the LOINC codes in use are the code list and each sending application is a section of the mapping table:

```ini
# config/user-conf/loinc.ini

[codes]
8302-2  = Body height
29463-7 = Body weight
39156-5 = Body mass index
8867-4  = Heart rate
```

```ini
# config/user-conf/observations.ini

[WELLNESS_APP]
HEIGHT-CM = 8302-2
WEIGHT-KG = 29463-7
BMI       = 39156-5
PULSE-BPM = 8867-4

[CHECKUP_KIOSK]
BODY-HT = 8302-2
BODY-WT = 29463-7
```

In the service, the sending application from `MSH-3` is the source and the observation identifier from `OBX-3` is the code:

```python
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class RecordObservation(Service):

    def handle(self):

        message = self.request.input

        sending_application = message.msh.sending_application
        local_code = message.obx.observation_identifier.identifier

        loinc_code = self.config.observations.translate(
            source=sending_application, code=local_code, codes='loinc')

        if loinc_code is None:
            self.logger.warning('Code `%s` from `%s` has no LOINC code', local_code, sending_application)
            return

        loinc_name = self.config.loinc.codes[loinc_code]
        self.logger.info('Code `%s` is LOINC `%s` (%s)', local_code, loinc_code, loinc_name)
```

A new sending application is a new section in `observations.ini`. No service is changed or redeployed.

## Editing config tables in the dashboard {#editing-config-tables-in-the-dashboard}

Config tables are edited in the dashboard under **Services** > **Config tables**, and [Config tables in the dashboard](https://zato.io/docs/dev/config/dashboard.html) describes the editor. Saving a file applies it on all servers without a restart.

![The config tables editor](https://zatosource-production.b-cdn.net/docs/gfx/service/config-tables.webp?v=1788288467)

Dashboard menu: Services > Config tables

## See also {#see-also}

- [Config files](https://zato.io/docs/dev/examples/config.html) - Configuration files in config/user-conf
- [Config tables in the dashboard](https://zato.io/docs/dev/config/dashboard.html) - The dashboard editor for config tables
- [Messages](https://zato.io/docs/dev/examples/messages.html) - Build the payloads the translated codes go into

## Learn more {#learn-more}

- [REST integration overview](https://zato.io/integrations/rest/) - Start here to see how teams build REST integrations
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - Master architectural patterns for production-ready integrations
- [Modern REST API tutorial](https://zato.io/tutorials/rest-api/python.html) - Design, build and secure REST APIs with Python
