UNPKG

@graphql-markdown/core

Version:

GraphQL-Markdown core package for generating Markdown documentation from a GraphQL schema.

216 lines (215 loc) 7.61 kB
"use strict"; /** * Option builder for handling CLI/config/default option precedence. * * This utility provides a fluent interface for merging options from multiple * sources (CLI, config file, defaults) with consistent precedence rules: * 1. CLI options (highest priority) * 2. Config file options * 3. Default values (lowest priority) * * Precedence is enforced semantically via the setIfProvided() method, * regardless of the order in which methods are called. Each value tracks its * source, and only higher-precedence sources can override existing values. * * Eliminates repetitive if/coalesce patterns throughout the codebase. * * @packageDocumentation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OptionBuilder = void 0; /** * Builder for constructing options from multiple sources with priority precedence. * * Precedence is enforced semantically via source tracking, not by call order. * CLI values always override config values, which always override defaults, * regardless of the order in which methods are called. * * @template T - The type of the options object being built * * @example * ```typescript * const options = new OptionBuilder<MyOptions>() * // Add in order: default -> config -> cli * .addDefault(false, "pretty") * .addFromConfig(undefined, "pretty") * .addFromCli(true, "pretty") // CLI overrides default * .addDefault("/", "baseURL") * .addFromConfig("/api", "baseURL") // Config overrides default * .addFromCli(undefined, "baseURL") * .build(); * // Result: { pretty: true, baseURL: "/api" } * ``` */ class OptionBuilder { static PRECEDENCE_ORDER = { default: 0, config: 1, cli: 2, }; merged = {}; sources = new Map(); /** * Adds a default value (lowest priority). * Sets a value that can be overwritten by config or CLI options. * This method can be called at any time; precedence is enforced semantically. * * @param value - The default value * @param key - The key to store the value under * @returns This builder for method chaining * * @example * ```typescript * builder.addDefault(3000, "port") * ``` */ addDefault(value, key) { return this.setIfProvided(value, key, "default"); } /** * Adds a value from config file if provided (medium priority). * Can override default values if the config value exists. * This method can be called at any time; precedence is enforced semantically. * * @param value - The config file option value * @param key - The key to store the value under * @returns This builder for method chaining * * @example * ```typescript * builder.addFromConfig(config.port, "port") * ``` */ addFromConfig(value, key) { return this.setIfProvided(value, key, "config"); } /** * Adds a value from CLI options if provided (highest priority). * Can override both default and config values for this key. * This method can be called at any time; precedence is enforced semantically. * * @param value - The CLI option value * @param key - The key to store the value under * @returns This builder for method chaining * * @example * ```typescript * builder.addFromCli(cliOpts.port, "port") * ``` */ addFromCli(value, key) { return this.setIfProvided(value, key, "cli"); } /** * Transforms a value using a function if the key exists. * Useful for processing values after they've been set. * * @param key - The key to transform * @param fn - Function that transforms the current value * @returns This builder for method chaining * * @example * ```typescript * builder.transform("path", (p) => resolve(p)) * ``` */ transform(key, fn) { if (key in this.merged) { this.merged[key] = fn(this.merged[key]); } return this; } /** * Conditionally applies a transformation if a predicate is true. * Useful for applying different transformations based on other values. * * @param key - The key to potentially transform * @param predicate - Function that determines if transformation should apply * @param fn - Function that transforms the value if predicate is true * @returns This builder for method chaining * * @example * ```typescript * builder.transformIf("path", () => force, (p) => resolve(p)) * ``` */ transformIf(key, predicate, fn) { if (key in this.merged && predicate({ ...this.merged })) { this.merged[key] = fn(this.merged[key]); } return this; } /** * Gets the current value for a key without building. * Useful for conditional logic during building. * Returns a shallow copy for arrays and objects to prevent external mutations. * Note: Nested objects and arrays are not deep copied. * * @param key - The key to get * @returns The current value for the key, or `undefined` if not set */ get(key) { const value = this.merged[key]; if (Array.isArray(value)) { return [...value]; } if (value && typeof value === "object") { return { ...value }; } return value; } /** * Returns the built options object with all accumulated values. * The object contains all keys that were set during the building process. * * Note: The returned object may be a partial object containing only the keys * that were set. Callers should handle potentially missing properties. * Returns a defensive copy of the object to prevent external mutations of * internal state. Arrays and objects are shallow-copied to protect against * external modifications. * * @returns The constructed options object with type `Partial<T>` (may not have all properties of `T`) */ build() { const result = {}; for (const key in this.merged) { const value = this.merged[key]; if (Array.isArray(value)) { result[key] = [...value]; } else if (value && typeof value === "object") { result[key] = { ...value }; } else { result[key] = value; } } return result; } /** * Sets a value if it's not null or undefined, respecting precedence. * Internal helper method used by addDefault, addFromConfig, and addFromCli. * * Only updates the merged value if the source has equal or higher precedence * than the currently stored source. Precedence order: `default < config < cli`. * * @param value - The value to set * @param key - The key to set * @param source - The source of this value ('default', 'config', or 'cli') * @returns This builder for method chaining */ setIfProvided(value, key, source) { if (value !== null && value !== undefined) { const currentSource = this.sources.get(key); const shouldUpdate = !currentSource || OptionBuilder.PRECEDENCE_ORDER[source] >= OptionBuilder.PRECEDENCE_ORDER[currentSource]; if (shouldUpdate) { this.merged[key] = value; this.sources.set(key, source); } } return this; } } exports.OptionBuilder = OptionBuilder;