1/**
2 * Config Internal
3 * ================
4 *
5 * The processed internal representation of a Command.Config declaration.
6 * Separates the user's declared config shape from the flat parsing representation.
7 *
8 * Key concepts:
9 * - ConfigInternal: The full processed form (flags, arguments, tree)
10 * - ConfigInternal.Tree: Maps declaration keys to nodes
11 * - ConfigInternal.Node: Param reference, Array, or Nested subtree
12 *
13 * Example transformation from Command.Config to Command.Config.Internal:
14 *
15 * ```ts
16 * // User declares:
17 * const config = {
18 * verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)),
19 * server: {
20 * host: Flag.String("host"),
21 * port: Flag.Int("port")
22 * },
23 * files: Argument.String("files").pipe(Argument.variadic)
24 * }
25 *
26 * // Becomes Config.Internal:
27 * {
28 * arguments: [filesParam], // Flat array of arguments
29 * flags: [verboseParam, hostParam, portParam], // Flat array of flags
30 * orderedParams: [verboseParam, hostParam, portParam, filesParam],
31 * tree: { // Preserves nested structure
32 * verbose: { _tag: "Param", index: 0 },
33 * server: {
34 * _tag: "Nested",
35 * tree: {
36 * host: { _tag: "Param", index: 1 },
37 * port: { _tag: "Param", index: 2 }
38 * }
39 * },
40 * files: { _tag: "Param", index: 3 }
41 * }
42 * }
43 * ```
44 *
45 * This separation allows:
46 * 1. Flat iteration over all params for parsing/validation
47 * 2. Reconstruction of original nested shape for handler input
48 */
49import * as InternalRecord from "../../internal/record.ts"
50import * as Param from "../Param.ts"
51
52/* ========================================================================== */
53/* Type ID */
54/* ========================================================================== */
55
56const ConfigInternalTypeId = "~effect/cli/Command/Config/Internal" as const
57
58/* ========================================================================== */
59/* Types */
60/* ========================================================================== */
61
62/**
63 * The processed internal representation of a Command.Config declaration.
64 *
65 * Created by parsing the user's config. Separates parameters by type
66 * while preserving the original nested structure via the tree.
67 */
68export interface ConfigInternal {
69 readonly [ConfigInternalTypeId]: typeof ConfigInternalTypeId
70 /** The command's positional argument parameters. */
71 readonly arguments: ReadonlyArray<Param.AnyArgument>
72 /** The command's flag parameters. */
73 readonly flags: ReadonlyArray<Param.AnyFlag>
74 /** All parameters in declaration order. */
75 readonly orderedParams: ReadonlyArray<Param.Any>
76 /** Tree structure for reconstructing nested config. */
77 readonly tree: ConfigInternal.Tree
78}
79
80/**
81 * Namespace containing the tree and node types used to reconstruct parsed
82 * command configuration values.
83 *
84 * @since 4.0.0
85 */
86export declare namespace ConfigInternal {
87 /**
88 * Maps declaration keys to their node representations.
89 * Preserves the shape of the user's config object.
90 */
91 export interface Tree {
92 [key: string]: Node
93 }
94
95 /**
96 * A node in the config tree.
97 *
98 * - Param: References a parameter by index in orderedParams
99 * - Array: Contains child nodes for tuple/array declarations
100 * - Nested: Contains a subtree for nested config objects
101 */
102 export type Node =
103 | { readonly _tag: "Param"; readonly index: number }
104 | { readonly _tag: "Array"; readonly children: ReadonlyArray<Node> }
105 | { readonly _tag: "Nested"; readonly tree: Tree }
106}
107
108/* ========================================================================== */
109/* Parsing */
110/* ========================================================================== */
111
112/**
113 * Config interface matching Command.Config (duplicated to avoid circular import).
114 * @internal
115 */
116interface Config {
117 readonly [key: string]:
118 | Param.Param<Param.ParamKind, any>
119 | ReadonlyArray<Param.Param<Param.ParamKind, any> | Config>
120 | Config
121}
122
123/**
124 * Parses a Command.Config into a ConfigInternal.
125 *
126 * Walks the config structure and:
127 * 1. Extracts all Params into flat arrays (flags, arguments, orderedParams)
128 * 2. Builds a tree that remembers the original nested structure
129 * 3. Assigns each Param an index to link parsed values back
130 *
131 * @internal
132 */
133export const parseConfig = (config: Config): ConfigInternal => {
134 const orderedParams: Array<Param.Any> = []
135 const flags: Array<Param.AnyFlag> = []
136 const args: Array<Param.AnyArgument> = []
137
138 function parse(config: Config): ConfigInternal.Tree {
139 const tree: ConfigInternal.Tree = Object.create(null)
140 for (const key of Object.keys(config)) {
141 tree[key] = parseValue(config[key])
142 }
143 return tree
144 }
145
146 function parseValue(
147 value: Param.Any | ReadonlyArray<Param.Any | Config> | Config
148 ): ConfigInternal.Node {
149 if (Array.isArray(value)) {
150 return {
151 _tag: "Array",
152 children: (value as Array<any>).map((v) => parseValue(v))
153 }
154 } else if (Param.isParam(value)) {
155 const index = orderedParams.length
156 orderedParams.push(value)
157
158 if (value.kind === "argument") {
159 args.push(value as Param.AnyArgument)
160 } else {
161 flags.push(value as Param.AnyFlag)
162 }
163
164 return { _tag: "Param", index }
165 } else {
166 return {
167 _tag: "Nested",
168 tree: parse(value as Config)
169 }
170 }
171 }
172
173 return {
174 [ConfigInternalTypeId]: ConfigInternalTypeId,
175 flags,
176 arguments: args,
177 orderedParams,
178 tree: parse(config)
179 }
180}
181
182/**
183 * @internal
184 */
185export const emptyConfig: ConfigInternal = parseConfig({})
186
187const shiftNodeIndexes = (node: ConfigInternal.Node, offset: number): ConfigInternal.Node => {
188 switch (node._tag) {
189 case "Param":
190 return {
191 _tag: "Param",
192 index: node.index + offset
193 }
194 case "Array":
195 return {
196 _tag: "Array",
197 children: node.children.map((child) => shiftNodeIndexes(child, offset))
198 }
199 case "Nested":
200 return {
201 _tag: "Nested",
202 tree: shiftTreeIndexes(node.tree, offset)
203 }
204 }
205}
206
207const shiftTreeIndexes = (tree: ConfigInternal.Tree, offset: number): ConfigInternal.Tree => {
208 const output: ConfigInternal.Tree = Object.create(null)
209 for (const key of Object.keys(tree)) {
210 output[key] = shiftNodeIndexes(tree[key], offset)
211 }
212 return output
213}
214
215/**
216 * @internal
217 */
218export const mergeConfig = (
219 left: ConfigInternal,
220 right: ConfigInternal
221): ConfigInternal => {
222 const offset = left.orderedParams.length
223 return {
224 [ConfigInternalTypeId]: ConfigInternalTypeId,
225 flags: [...left.flags, ...right.flags],
226 arguments: [...left.arguments, ...right.arguments],
227 orderedParams: [...left.orderedParams, ...right.orderedParams],
228 tree: Object.assign(Object.create(null), left.tree, shiftTreeIndexes(right.tree, offset))
229 }
230}
231
232/* ========================================================================== */
233/* Reconstruction */
234/* ========================================================================== */
235
236/**
237 * Reconstructs the original nested config shape from parsed values.
238 *
239 * Uses the tree as a blueprint to place each parsed value back into
240 * its original position in the nested structure.
241 *
242 * @internal
243 */
244export const reconstructTree = (
245 tree: ConfigInternal.Tree,
246 results: ReadonlyArray<any>
247): Record<string, any> => {
248 const output: Record<string, any> = {}
249
250 for (const key of Object.keys(tree)) {
251 InternalRecord.assignProperty(output, key, nodeValue(tree[key]))
252 }
253
254 return output
255
256 function nodeValue(node: ConfigInternal.Node): any {
257 switch (node._tag) {
258 case "Param":
259 return results[node.index]
260 case "Array":
261 return node.children.map((child) => nodeValue(child))
262 case "Nested":
263 return reconstructTree(node.tree, results)
264 }
265 }
266}