1 | import {find} from 'lodash'
|
2 | import {SerializePath, SerializeOptions} from './StructureNodes'
|
3 | import {ChildResolverOptions, ChildResolver} from './ChildResolver'
|
4 | import {SerializeError, HELP_URL} from './SerializeError'
|
5 | import {ListItem, ListItemBuilder} from './ListItem'
|
6 | import {
|
7 | GenericListBuilder,
|
8 | BuildableGenericList,
|
9 | GenericList,
|
10 | GenericListInput
|
11 | } from './GenericList'
|
12 |
|
13 | const getArgType = (thing: ListItem) => {
|
14 | return Array.isArray(thing) ? 'array' : typeof thing
|
15 | }
|
16 |
|
17 | const resolveChildForItem: ChildResolver = (itemId: string, options: ChildResolverOptions) => {
|
18 | const parentItem = options.parent as List
|
19 | const target = (parentItem.items.find(item => item.id === itemId) || {child: undefined}).child
|
20 | if (!target || typeof target !== 'function') {
|
21 | return target
|
22 | }
|
23 |
|
24 | return typeof target === 'function' ? target(itemId, options) : target
|
25 | }
|
26 |
|
27 | function maybeSerializeListItem(
|
28 | item: ListItem | ListItemBuilder,
|
29 | index: number,
|
30 | path: SerializePath
|
31 | ): ListItem {
|
32 | if (item instanceof ListItemBuilder) {
|
33 | return item.serialize({path, index})
|
34 | }
|
35 |
|
36 | const listItem = item as ListItem
|
37 | if (!listItem || listItem.type !== 'listItem') {
|
38 | const gotWhat = (listItem && listItem.type) || getArgType(listItem)
|
39 | const helpText = gotWhat === 'array' ? ' - did you forget to spread (...moreItems)?' : ''
|
40 | throw new SerializeError(
|
41 | `List items must be of type "listItem", got "${gotWhat}"${helpText}`,
|
42 | path,
|
43 | index
|
44 | ).withHelpUrl(HELP_URL.INVALID_LIST_ITEM)
|
45 | }
|
46 |
|
47 | return item
|
48 | }
|
49 |
|
50 | export interface List extends GenericList {
|
51 | items: ListItem[]
|
52 | }
|
53 |
|
54 | export interface ListInput extends GenericListInput {
|
55 | items?: (ListItem | ListItemBuilder)[]
|
56 | }
|
57 |
|
58 | export interface BuildableList extends BuildableGenericList {
|
59 | items?: (ListItem | ListItemBuilder)[]
|
60 | }
|
61 |
|
62 | export class ListBuilder extends GenericListBuilder<BuildableList, ListBuilder> {
|
63 | protected spec: BuildableList
|
64 |
|
65 | constructor(spec?: ListInput) {
|
66 | super()
|
67 | this.spec = spec ? spec : {}
|
68 | }
|
69 |
|
70 | items(items: (ListItemBuilder | ListItem)[]): ListBuilder {
|
71 | return this.clone({items})
|
72 | }
|
73 |
|
74 | getItems() {
|
75 | return this.spec.items
|
76 | }
|
77 |
|
78 | serialize(options: SerializeOptions = {path: []}): List {
|
79 | const id = this.spec.id
|
80 | if (typeof id !== 'string' || !id) {
|
81 | throw new SerializeError(
|
82 | '`id` is required for lists',
|
83 | options.path,
|
84 | options.index
|
85 | ).withHelpUrl(HELP_URL.ID_REQUIRED)
|
86 | }
|
87 |
|
88 | const items = typeof this.spec.items === 'undefined' ? [] : this.spec.items
|
89 | if (!Array.isArray(items)) {
|
90 | throw new SerializeError(
|
91 | '`items` must be an array of items',
|
92 | options.path,
|
93 | options.index
|
94 | ).withHelpUrl(HELP_URL.LIST_ITEMS_MUST_BE_ARRAY)
|
95 | }
|
96 |
|
97 | const path = (options.path || []).concat(id)
|
98 | const serializedItems = items.map((item, index) => maybeSerializeListItem(item, index, path))
|
99 | const dupes = serializedItems.filter((val, i) => find(serializedItems, {id: val.id}, i + 1))
|
100 |
|
101 | if (dupes.length > 0) {
|
102 | const dupeIds = dupes.map(item => item.id).slice(0, 5)
|
103 | const dupeDesc = dupes.length > 5 ? `${dupeIds.join(', ')}...` : dupeIds.join(', ')
|
104 | throw new SerializeError(
|
105 | `List items with same ID found (${dupeDesc})`,
|
106 | options.path,
|
107 | options.index
|
108 | ).withHelpUrl(HELP_URL.LIST_ITEM_IDS_MUST_BE_UNIQUE)
|
109 | }
|
110 |
|
111 | return {
|
112 | ...super.serialize(options),
|
113 | type: 'list',
|
114 | child: this.spec.child || resolveChildForItem,
|
115 | items: serializedItems
|
116 | }
|
117 | }
|
118 |
|
119 | clone(withSpec?: BuildableList) {
|
120 | const builder = new ListBuilder()
|
121 | builder.spec = {...this.spec, ...(withSpec || {})}
|
122 | return builder
|
123 | }
|
124 | }
|