UNPKG

4.21 kBPlain TextView Raw
1import {find} from 'lodash'
2import {SerializePath, SerializeOptions, Divider} from './StructureNodes'
3import {ChildResolverOptions, ChildResolver} from './ChildResolver'
4import {SerializeError, HELP_URL} from './SerializeError'
5import {ListItem, ListItemBuilder} from './ListItem'
6import {
7 GenericListBuilder,
8 BuildableGenericList,
9 GenericList,
10 GenericListInput
11} from './GenericList'
12
13const 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
25const isListItem = (item: ListItem | Divider): item is ListItem => {
26 return item.type === 'listItem'
27}
28
29const 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
41function 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
68function isPromise<T>(thing: any): thing is Promise<T> {
69 return thing && typeof thing.then === 'function'
70}
71
72export interface List extends GenericList {
73 items: (ListItem | Divider)[]
74}
75
76export interface ListInput extends GenericListInput {
77 items?: (ListItem | ListItemBuilder | Divider)[]
78}
79
80export interface BuildableList extends BuildableGenericList {
81 items?: (ListItem | ListItemBuilder | Divider)[]
82}
83
84export 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}