Highly ambitious ATProtocol AppView service and sdks
at main 134 lines 3.8 kB view raw
1import { resolve, dirname } from "@std/path"; 2import { existsSync } from "@std/fs"; 3import { logger } from "./logger.ts"; 4 5export interface SlicesConfig { 6 slice?: string; 7 apiUrl?: string; 8 lexiconPath?: string; 9 clientOutputPath?: string; 10} 11 12export class SlicesConfigLoader { 13 private configCache: SlicesConfig | null = null; 14 private configPath: string | null = null; 15 16 /** 17 * Find and load slices.json config file, starting from current directory 18 * and walking up the directory tree 19 */ 20 async load(startPath = Deno.cwd()): Promise<SlicesConfig> { 21 if (this.configCache && this.configPath) { 22 return this.configCache; 23 } 24 25 const configPath = this.findConfigFile(startPath); 26 if (!configPath) { 27 logger.debug("No slices.json config file found"); 28 return {}; 29 } 30 31 this.configPath = configPath; 32 logger.debug(`Loading config from: ${configPath}`); 33 34 try { 35 const configText = await Deno.readTextFile(configPath); 36 const config = JSON.parse(configText) as SlicesConfig; 37 38 // Validate config structure 39 this.validateConfig(config); 40 41 this.configCache = config; 42 return config; 43 } catch (error) { 44 const err = error as Error; 45 logger.warn(`Failed to load config file ${configPath}: ${err.message}`); 46 return {}; 47 } 48 } 49 50 /** 51 * Get the directory containing the config file 52 */ 53 getConfigDir(): string | null { 54 return this.configPath ? dirname(this.configPath) : null; 55 } 56 57 /** 58 * Clear the config cache (useful for testing) 59 */ 60 clearCache(): void { 61 this.configCache = null; 62 this.configPath = null; 63 } 64 65 /** 66 * Find slices.json file by walking up the directory tree 67 */ 68 private findConfigFile(startPath: string): string | null { 69 let currentPath = resolve(startPath); 70 let lastPath = ""; 71 72 while (currentPath !== lastPath) { 73 const configPath = resolve(currentPath, "slices.json"); 74 if (existsSync(configPath)) { 75 return configPath; 76 } 77 78 lastPath = currentPath; 79 currentPath = dirname(currentPath); 80 } 81 82 return null; 83 } 84 85 /** 86 * Validate the config file structure 87 */ 88 private validateConfig(config: unknown): void { 89 if (typeof config !== "object" || config === null) { 90 throw new Error("Config must be an object"); 91 } 92 93 const cfg = config as Record<string, unknown>; 94 95 if (cfg.slice !== undefined && typeof cfg.slice !== "string") { 96 throw new Error("Config 'slice' must be a string"); 97 } 98 99 if (cfg.apiUrl !== undefined && typeof cfg.apiUrl !== "string") { 100 throw new Error("Config 'apiUrl' must be a string"); 101 } 102 103 if (cfg.lexiconPath !== undefined && typeof cfg.lexiconPath !== "string") { 104 throw new Error("Config 'lexiconPath' must be a string"); 105 } 106 107 if (cfg.clientOutputPath !== undefined && typeof cfg.clientOutputPath !== "string") { 108 throw new Error("Config 'clientOutputPath' must be a string"); 109 } 110 111 // Validate slice URI format if provided 112 if (cfg.slice && typeof cfg.slice === "string") { 113 if (!cfg.slice.startsWith("at://")) { 114 throw new Error("Config 'slice' must be a valid AT URI (starting with 'at://')"); 115 } 116 } 117 } 118} 119 120/** 121 * Merge command line arguments with config file values 122 * Command line arguments take precedence over config file 123 */ 124export function mergeConfig( 125 config: SlicesConfig, 126 args: Record<string, unknown> 127): SlicesConfig { 128 return { 129 slice: (args.slice as string) || config.slice, 130 apiUrl: (args["api-url"] as string) || config.apiUrl || "https://api.slices.network", 131 lexiconPath: (args.path as string) || config.lexiconPath || "./lexicons", 132 clientOutputPath: (args.output as string) || config.clientOutputPath || "./generated_client.ts", 133 }; 134}