You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# 9. Separate configurable document forms and presenters from attribute schema
2
+
3
+
## Status
4
+
5
+
Accepted
6
+
7
+
## Context
8
+
9
+
The configurable document schema introduced in [ADR-0006](./0006-config-driven-content-types.md) and evolved in [ADR-0007](./0007-use-rails-validation-for-configurable-documents.md) and [ADR-0008](./0008-drop-json-schema-for-document-configuration.md) defined document properties under `schema.properties` and grouped UI fields via `settings.edit_screens`. This conflated three concerns:
10
+
11
+
- How publishers see and group fields in the UI (form layout and copy).
12
+
- How field values are presented to downstream systems (e.g. Publishing API payload mapping and transformations).
13
+
- How attributes and validations are defined and enforced.
14
+
15
+
This coupling made it hard to:
16
+
17
+
- Group related fields in the UI without affecting payload mapping.
18
+
- Present the same attribute differently to publishers and Publishing API. E.g Duration block in topical events holding data for `start_date` and `end_date` but needed to be presented as individual fields to publishing API.
19
+
- Flatten validations while still allowing nested form structures, leading to complex validation logic.
20
+
21
+
## Decision
22
+
23
+
We refactored the configurable document type schema to separate concerns:
24
+
25
+
1. Introduced a top-level `forms` hash describing UI forms and their fields (label, description, block type), replacing `settings.edit_screens`.
26
+
2. Replaced `schema.properties` with `schema.attributes`. The current implementation focuses on simple types (string, integer, date), moving away from nested object properties. In future, we are open to introducing more complex attribute shapes like arrays.
27
+
3. Introduced a top-level `presenters` hash to describe how attribute values are mapped or transformed for downstream consumers (e.g. Publishing API), helping to decouple UI layout from payload shape.
28
+
4. Moved validation definitions to `schema.validations`, listing validators by attribute name. Nested validations within property definitions are no longer used in current schemas to simplify validation logic.
29
+
5. Removed custom nested block content handling for `object` types. Object attributes now use Rails' default hash implementation.
30
+
6. Updated the configurable document type JSON schema to validate the new structure and removed obsolete nested-schema code paths.
31
+
32
+
## Consequences
33
+
34
+
- All configurable document type definitions must use `schema.attributes`, `forms`, and `presenters`; `settings.edit_screens` and nested property validations are no longer supported.
35
+
- UI layout changes (field grouping, titles, descriptions, block types) can now be made in `forms` without impacting Publishing API payloads, which are defined in `presenters`.
36
+
- Validators will run against the flattened `schema.attributes` namespace. Added tests ensure nested data values stored in block content are preserved when present in the payload.
37
+
- Future support for genuinely nested attribute schemas or arrays would need a new design rather than reintroducing `object` types.
38
+
- It now becomes easier to present the same attribute differently in the UI and Publishing API (or any other consumer in future) by defining separate mappings in `forms` and `presenters`.
39
+
40
+
## Example
41
+
42
+
A configurable document type schema following the new structure will be identical to this:
43
+
44
+
```json
45
+
{
46
+
"key": "example_document_type",
47
+
"title": "Example Document Type",
48
+
"description": "An example document type with configurable forms and presenters",
Copy file name to clipboardExpand all lines: docs/configurable_document_types.md
+77-51Lines changed: 77 additions & 51 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,15 +15,16 @@ Configurable document types are defined as JSON. The JSON files are stored in th
15
15
The JSON for each type has these top level keys:
16
16
17
17
- 'key': The unique identifier for the document type. This is what will be stored in the edition's `configurable_document_type` column.
18
-
- 'schema': The schema for the document type. Each type must have a root schema of the type "object". This root object contains the `properties` for the document type, which map to [content blocks](#content-blocks) in the application.
18
+
- 'forms': The forms configuration for the document type. This describes how the document type's fields are presented on the UI. Each form (e.g., `documents`, `images`) contains a hash of `fields` hash whose keys should match attributes defined in `schema.attributes`. See [Block Rendering](#block-rendering) section for more details on how forms are rendered.
19
+
- 'schema': The schema for the document type. It contains `attributes` to define the data type for each form field and `validations` to specify what validations to run against the attributes. More details about attributes in [content blocks](#content-blocks).
20
+
- 'presenters': The presenters for the document type. This helps to define a liist of presenters that will consume the document type's content. Each presenter contains a set of schema keys (as defined in `schema.attributes`) whose values should map to their presenter's corresponding BlockContent payload builder method - see the [Publishing API Payload](#publishing-api-payload) section for more details.
19
21
- 'associations': The associations for the document type. This is a list of strings that map to a set of association objects in the Rails app. All associations are included as concerns on the corresponding edition model (such as `StandardEdition`), and then required depending on whether they are included in the document type's configuration.
20
22
- 'settings': A set of configurations for the content type, including edition behaviours that we want to turn on, downstream information or admin-side rendering details.
21
23
22
24
These are the settings available for configurable document types.
| edit_screens | Y | A list of rendered screens (tabs) for the content type. Controls which content blocks appear on which editing screens (such as the document and images tabs). |
27
28
| base_path_prefix | Y | The prefix for the base path at which the document will be published e.g. '/government/history for the page /government/history/10-downing-street'. |
28
29
| configurable_document_group | N | Optional grouping for document types. Used for categorisation in the admin interface, including search filters. Typically matches the `publishing_api_schema_name`. For example, both `news_story` and `government_response` types would have the group `news_article`. |
29
30
| publishing_api_schema_name | Y | Name of the schema in the Publishing API for this document type. Different types might share the same schema name. For example, `news_story` and `government_response` types both use the `news_article` schema. |
@@ -39,41 +40,40 @@ These are the settings available for configurable document types.
39
40
40
41
## Content Blocks
41
42
42
-
Each property within a configurable document schema is represented in the application as a content block. The content block is specified via the "type" and "format" options on the property schema. Content blocks are defined in the `app/models/configurable_content_blocks` directory.
43
+
Each attribute in `schema.attributes` holds the data type for its corresponding content block.
44
+
Content blocks are specified via the "block" property in the forms configuration. All available content blocks are defined in the `app/models/configurable_content_blocks` directory.
43
45
44
46
NB: The `title` and `summary` attributes for standard editions are not stored in the block content, but rather as first-class attributes on the edition model itself.
45
47
46
48
Content blocks are instantiated via the [content block factory](../app/models/configurable_content_blocks/factory.rb) at the point of rendering the form or generating the Publishing API payload. The factory uses the `type` and `format` specified in the schema to determine which block class to instantiate. The block's `type` is used for automatic type casting in the [block content model](../app/models/standard_edition/block_content.rb).
47
49
48
50
Each content block implements the following methods:
49
-
50
-
-`publishing_api_payload(content)`: Returns the value to be sent to Publishing API for the property. This can return any type. If returning a hash, ensure you use symbols for the keys.
51
51
-`to_partial_path`: Returns the path to the view that renders the form control for the property.
52
52
53
53
### Block rendering
54
54
55
-
When we render the [form](../app/views/admin/standard_editions/_form.html.erb) for a standard edition, we initialise a "root" block" of type `object`. When this root block's [view](../app/views/admin/configurable_content_blocks/_default_object.html.erb) gets rendered at the view specified in the `DefaultObject`'s block `to_partial_path`, we loop through the block's schema properties and in turn initialise and render blocks matching the specified schemas. The blocks will render in the order they are defined in the schema.
55
+
When we render the [form](../app/views/admin/standard_editions/_form.html.erb) for a standard edition, we initialise a "root" block" of type `object`. When this root block's [view](../app/views/admin/configurable_content_blocks/_default_object.html.erb) gets rendered at the view specified in the `DefaultObject`'s block `to_partial_path`, we loop through the block's schema properties and in turn initialise and render blocks matching the specified schemas. The blocks will render in the order they are defined in the forms property.
56
56
57
-
The default object block is a recursive block type, as it can contain other **object** blocks within its properties. This allows us to build arbitrarily deep trees of content blocks, as defined by the document type's schema. The following is an example of a simple schema with nested object properties:
57
+
The default object block is a recursive block type, as it can contain other **object** blocks within its properties. This allows us to build arbitrarily deep trees of content blocks, as defined by the document type's schema. The following is an example of a forms configuration that would display two form tabs on the standard edition form (i.e "documents" and "images"). The "documents" tab contains a `Govspeak` block, and the "images" tab contains an `ImageSelect` block nested within a `DefaultObject` block.
58
58
59
59
```json
60
60
{
61
-
"root_object": {
62
-
"title": "Root Object",
63
-
"type": "object",
64
-
"properties": {
65
-
"leaf_property_one": {
66
-
"title": "Leaf Property One",
67
-
"type": "string",
68
-
"format": "govspeak"
69
-
},
70
-
"object_property": {
71
-
"properties": {
72
-
"leaf_property_two": {
73
-
"title": "Leaf Property Two",
74
-
"type": "string",
75
-
"format": "image_select"
76
-
}
61
+
"forms": {
62
+
"documents": {
63
+
"fields": {
64
+
"body": {
65
+
"title": "Body",
66
+
"description": "The main content of the page",
67
+
"block": "govspeak"
68
+
}
69
+
}
70
+
},
71
+
"images": {
72
+
"fields": {
73
+
"image": {
74
+
"title": "Image",
75
+
"description": "Select an image to display on the page",
76
+
"block": "image_select"
77
77
}
78
78
}
79
79
}
@@ -82,12 +82,10 @@ The default object block is a recursive block type, as it can contain other **ob
82
82
```
83
83
The rendering process would be as follows:
84
84
- renders the standard edition [form](../app/views/admin/standard_editions/_form.html.erb)
85
-
- renders `root_object` which is a `DefaultObject` block (using the `_default_object.html.erb` partial)
86
-
- loops through the `root_object` properties:
87
-
- renders `leaf_property_one` which is a `Govspeak` block (using the `_govspeak.html.erb` partial)
88
-
- renders `object_property` which is a `DefaultObject` block (using the `_default_object.html.erb` partial)
89
-
- loops through the `object_property` properties:
90
-
- renders `leaf_property_two` which is an `ImageSelect` block (using the `_image_select.html.erb` partial)
85
+
- Loops through each form `documents` and `images`, each of which is a `DefaultObject` block (using the `_default_object.html.erb` partial)
86
+
- loops through the `fields` properties:
87
+
- renders each field e.g `body` which is a `Govspeak` block (using the `_govspeak.html.erb` partial)
88
+
91
89
92
90
Block views use the following [partial-local](https://guides.rubyonrails.org/action_view_overview.html#passing-data-to-partials-with-locals-option) variables:
93
91
- The property `schema` and `content` (default `{}`) are provided for the specific part of the tree being rendered by the content block. The content is for the edition's primary locale.
@@ -100,8 +98,26 @@ Block views use the following [partial-local](https://guides.rubyonrails.org/act
100
98
101
99
### Publishing API Payload
102
100
103
-
In order to compose the `details` hash for Publishing API, The `StandardEditionPresenter`calls the `DefaultObject`'s `publishing_api_payload`. This then loops through all its nested blocks, calling their `publishing_api_payload` methods respectively.
104
-
Relevant payload settings for Publishing API are `base_path_prefix`, `publishing_api_schema_name`, and `publishing_api_document_type`, defined in the document type's [configuration](#document-type-configuration).
101
+
The `StandardEditionPresenter` uses the document type's `presenters` configuration to compose the `details` hash for Publishing API. The `presenters` hash maps each attribute (defined in `schema.attributes`) to its corresponding block content payload builder method.
102
+
103
+
For example, a presenter configuration will look like:
104
+
105
+
```json
106
+
{
107
+
"presenters": {
108
+
"publishing_api": {
109
+
"body": "govspeak",
110
+
"image": "image_select"
111
+
}
112
+
}
113
+
}
114
+
```
115
+
116
+
This instructs the presenter that the `body` attribute should use the `govspeak` payload builder and the `image` attribute should use the `image_select` payload builder.
117
+
118
+
Each block type implements its own publishing API payload method (see [app/presenters/publishing_api/payload_builder/block_content.rb](../app/presenters/publishing_api/payload_builder/block_content.rb)), which is called for the attributes configured in the presenter.
119
+
120
+
This separation allows the same attribute to be presented differently in the UI (via the forms configuration) and in the Publishing API payload, providing flexibility for future changes to either without affecting the other.
105
121
106
122
### Create a new content block type
107
123
@@ -113,12 +129,20 @@ Relevant payload settings for Publishing API are `base_path_prefix`, `publishing
113
129
- The name of the partial should correspond to the block class name.
114
130
115
131
### Using a content block in the schema
116
-
To use a block, include it in the schema with the following properties:
117
-
-`title`: Display label for the content block.
118
-
-`description`: (Optional) Description of the content block's purpose. Usually used for hint text.
119
-
-`type`: Data type for the content block. Should map to an active record type, with the exception of the `object` and `array` type.
120
-
-`format`: Format for the content block, e.g., 'govspeak' for rich text, 'image_select' for image picker. Formats represent specific use cases of some of the types. This must match one of the formats registered in the [blocks factory](../app/models/configurable_content_blocks/factory.rb).
121
-
-`validations`: (Optional) Validations to be applied to the content block. See the [Content Block Validation](#content-block-validation) section for more details.
132
+
To use a content block, you need to define it in both the schema and forms:
133
+
134
+
- Define the UI in `forms.<form_tab>.fields.<field_name>`:
135
+
-`title`: Display label for the field.
136
+
-`description`: (Optional) Help text shown to the user.
137
+
-`block`: Block component to use (e.g., `govspeak`, `image_select`). Must match a format registered in the [blocks factory](../app/models/configurable_content_blocks/factory.rb).
138
+
139
+
- Define the data type in `schema.attributes.<field_name>`:
140
+
-`type`: Data type for the attribute (e.g., `string`, `integer`, `date`). Used for type casting in the [block content model](../app/models/standard_edition/block_content.rb).
141
+
142
+
- Define the Publishing API mapping in `presenters.publishing_api`:
143
+
- Maps the attribute name to its payload builder method (e.g., `"body": "govspeak"`).
144
+
145
+
- Add other presenters as needed, following the same mapping structure above.
0 commit comments