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