Skip to content

Commit 2dc4c84

Browse files
committed
feat: Refactored resolver layout and logic
1 parent 307e13a commit 2dc4c84

File tree

15 files changed

+193
-146
lines changed

15 files changed

+193
-146
lines changed

src/api/dashboard/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { config } from '../../config.js';
22
import { LoginHelper } from '../../connect/login-helper.js';
3-
import { CloudDocument } from './types.js';
3+
import { RemoteDocument } from './types.js';
44
import { UnauthorizedError } from '../../common/errors.js';
55

66
export const DashboardApiClient = {
7-
async getDocument(id: string): Promise<CloudDocument> {
7+
async getDocument(id: string): Promise<RemoteDocument> {
88
const login = LoginHelper.get()?.credentials;
99
if (!login) {
1010
throw new Error('Not logged in');

src/api/dashboard/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Config } from 'codify-schemas';
22

3-
export interface CloudDocument {
3+
export interface RemoteDocument {
44
id: string;
55
contents: Array<Config>
66
}

src/common/initialize-plugins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ export class PluginInitOrchestrator {
8080
* Order:
8181
* 1. If path is specified, return that.
8282
* 2. If path is a dir with only one *codify.json|*codify.jsonc|*codify.json5|*codify.yaml, return that.
83-
* 3. If path is a UUID, return file from Codify cloud.
83+
* 3. If path is a UUID, return file from Codify remote.
8484
* 4. If multiple exists in the path (dir), then prompt the user to select one.
8585
* 5. If no path is provided, run steps 2 - 4 for the current dir.
86-
* 6. If none exists, return default file from codify cloud.
86+
* 6. If none exists, return default file from codify remote.
8787
* 7. If user is not logged in, return an error.
8888
*
8989
* Order:

src/orchestrators/import.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export class ImportOrchestrator {
271271
await sleep(100);
272272
}
273273

274-
// Special handling for codify cloud files. Import and refresh can automatically save file updates.
274+
// Special handling for codify remote files. Import and refresh can automatically save file updates.
275275
static async handleCodifyRemoteFiles(reporter: Reporter, importResult: ImportResult) {
276276
try {
277277
if (!importResult.result.some((r) => r.type === 'remote-file')) {
@@ -330,7 +330,7 @@ export class ImportOrchestrator {
330330
await ApiClient.updateRemoteFile(file.parameters.remote as string, new Blob([content]), credentials);
331331
}
332332

333-
ctx.log('Successfully uploaded changes to Codify cloud');
333+
ctx.log('Successfully uploaded changes to Codify remote');
334334
} catch {
335335
console.warn('Unable to process remote-files');
336336
}

src/parser/entities.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { Config } from 'codify-schemas';
22
import { SourceMapCache } from './source-maps.js';
33

44
export interface InMemoryFile {
5+
// The contents of the file
56
contents: string;
7+
// Path to the specific file
68
path: string;
9+
// The file type (json, yaml, json5, jsonc, remote)
710
fileType: FileType;
811
}
912

@@ -21,5 +24,5 @@ export enum FileType {
2124
YAML = 'yaml',
2225
JSON5 = 'json5',
2326
JSONC = 'jsonc',
24-
REMOTE = 'remote',
27+
REMOTE = 'remote', // Remote files are always JSONC for now.
2528
}

src/parser/errors.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { ResolverResult } from './resolvers.js';
2+
13
export class MultipleFilesError extends Error {
2-
files: string[]
4+
result: ResolverResult
35

4-
constructor(files: string[]) {
5-
super(`Multiple matching Codify files found:\n${files.join('\n')}`);
6-
this.files = files;
6+
constructor(result: ResolverResult) {
7+
super(`Multiple matching Codify files found:\n${result.files.join('\n')}`);
8+
this.result = result;
79
}
810
}
11+
12+
export class NoCodifyFileError extends Error {
13+
result: ResolverResult
14+
15+
constructor(result: ResolverResult) {
16+
super(`No Codify file found at ${result.location}`);
17+
this.result = result;
18+
}
19+
}

src/parser/index.ts

Lines changed: 44 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
import { Config } from 'codify-schemas';
2-
import * as fs from 'node:fs/promises';
3-
import path from 'node:path';
4-
import { validate } from 'uuid'
52

63
import { InternalError } from '../common/errors.js';
74
import { ConfigBlock } from '../entities/config.js';
85
import { Project } from '../entities/project.js';
9-
import { FileUtils } from '../utils/file.js';
10-
import { CloudParser } from './cloud/cloud-parser.js';
116
import { ConfigFactory } from './config-factory.js';
127
import { FileType, InMemoryFile, ParsedConfig } from './entities.js';
8+
import { MultipleFilesError, NoCodifyFileError } from './errors.js';
139
import { JsonParser } from './json/json-parser.js';
1410
import { Json5Parser } from './json5/json-parser.js';
1511
import { JsoncParser } from './jsonc/json-parser.js';
16-
import { CloudReader } from './reader/cloud-reader.js';
17-
import { FileReader } from './reader/file-reader.js';
12+
import { RemoteParser } from './remote/remote-parser.js';
13+
import { CodifyResolver, ResolverResult, ResolverType } from './resolvers.js';
1814
import { SourceMapCache } from './source-maps.js';
1915
import { YamlParser } from './yaml/yaml-parser.js';
20-
import { MultipleFilesError } from './errors.js';
21-
import { CodifyResolver, ResolverType } from './resolvers.js';
2216

2317
export const CODIFY_FILE_REGEX = /^(.*)?codify(.*)?(.json|.yaml|.json5|.jsonc)$/;
2418

@@ -28,11 +22,17 @@ export interface ParserArgs {
2822
path?: string;
2923
transformProject?: (project: Project) => Project | Promise<Project>;
3024
rawConfigs?: Config[]; // Raw configs are provided directly
25+
resolverType?: ResolverType;
3126
}
3227

33-
interface FilePointer {
34-
location: string;
35-
type: FileType;
28+
interface ParseResult {
29+
configs: ParsedConfig[];
30+
file: InMemoryFile;
31+
}
32+
33+
interface ConfigResult {
34+
configs: ConfigBlock[];
35+
file: InMemoryFile;
3636
}
3737

3838
class Parser {
@@ -41,7 +41,7 @@ class Parser {
4141
[FileType.YAML]: new YamlParser(),
4242
[FileType.JSON5]: new Json5Parser(),
4343
[FileType.JSONC]: new JsoncParser(),
44-
[FileType.REMOTE]: new CloudParser(),
44+
[FileType.REMOTE]: new RemoteParser(),
4545
}
4646

4747
/**
@@ -57,16 +57,15 @@ class Parser {
5757
* @param location
5858
* @param args
5959
*/
60-
async parse(location: string, args?: ParserArgs): Promise<Project> {
60+
async parse(location: string, args?: ParserArgs, isLoggedIn = false): Promise<Project> {
6161
const sourceMaps = new SourceMapCache()
6262

63-
const configs = this.resolveFiles(args)
64-
.then((result) => this.throwIfMultipleFiles(result))
65-
.then((path) => this.readFiles(codifyFiles))
63+
const { configs, file } = await this.resolveFiles(location, args, isLoggedIn)
64+
.then((result) => this.validateResolver(result))
6665
.then((files) => this.parseContents(files, sourceMaps))
6766
.then((config) => this.createConfigBlocks(config, sourceMaps))
6867

69-
return Project.create(configs, codifyFiles[0], sourceMaps);
68+
return Project.create(configs, file.path, sourceMaps);
7069
}
7170

7271
async parseJson(configs: Config[]): Promise<Project> {
@@ -77,83 +76,51 @@ class Parser {
7776
sourceMaps
7877
)
7978

80-
return Project.create(configBlocks, undefined, sourceMaps);
79+
return Project.create(configBlocks.configs, undefined, sourceMaps);
8180
}
8281

83-
private async getFilePaths(dirOrFile: string): Promise<string[]> {
84-
// A cloud file is represented as an uuid. Skip file checks if it's a cloud file;
85-
if (validate(dirOrFile)) {
86-
return [dirOrFile];
82+
private async resolveFiles(location: string, args?: ParserArgs, isLoggedIn = false): Promise<ResolverResult> {
83+
if (args?.resolverType) {
84+
return CodifyResolver.runResolver(location, args.resolverType);
8785
}
8886

89-
const absolutePath = path.resolve(dirOrFile);
90-
const isDirectory = (await fs.lstat(absolutePath)).isDirectory();
91-
92-
// A single file was passed in. We need to test if the file satisfies the codify file regex
93-
if (!isDirectory) {
94-
const fileName = path.basename(absolutePath);
95-
if (!CODIFY_FILE_REGEX.test(fileName)) {
96-
throw new Error(`Invalid file path provided ${absolutePath} ${fileName}. Expected the file to be *.codify.jsonc, *.codify.json5, *.codify.json, or *.codify.yaml `)
97-
}
98-
99-
return [absolutePath];
87+
if (args?.path) {
88+
return CodifyResolver.resolveLocal(args?.path)
10089
}
10190

102-
const filesInDir = await fs.readdir(absolutePath);
103-
104-
return filesInDir
105-
.filter((name) => CODIFY_FILE_REGEX.test(name))
106-
.map((name) => path.join(absolutePath, name))
107-
}
108-
109-
private async resolveFiles(location: string, args?: ParserArgs, isLoggedIn = false): Promise<string[]> {
110-
return CodifyResolver.runUntilResolves(location, [
111-
(args?.path) ? ResolverType.EXPLICIT_PATH : null,
112-
ResolverType.FILE_OR_DIRECTORY,
91+
return CodifyResolver.run(location, [
92+
ResolverType.LOCAL,
11393
(isLoggedIn) ? ResolverType.REMOTE_DOCUMENT_ID : null,
114-
(isLoggedIn) ? ResolverType.REMOTE_FILE : null,
115-
ResolverType.TEMPLATE,
94+
(isLoggedIn) ? ResolverType.REMOTE_DOCUMENT : null,
95+
(args?.allowTemplates) ? ResolverType.TEMPLATE : null,
11696
]);
117-
11897
}
11998

120-
private async throwIfMultipleFiles(result: string[]): Promise<string> {
121-
if (result.length > 1) {
122-
throw new MultipleFilesError(result);
99+
private async validateResolver(result: ResolverResult): Promise<InMemoryFile> {
100+
if (result.files.length === 0) {
101+
throw new NoCodifyFileError(result);
123102
}
124103

125-
return result[0];
126-
}
127-
128-
private readFiles(filePaths: string[]): Promise<InMemoryFile[]> {
129-
const cloudReader = new CloudReader();
130-
const fileReader = new FileReader();
131-
132-
return Promise.all(filePaths.map(
133-
async (p) => {
134-
// If path is a uuid and doesn't exist as a file, it's a cloud file
135-
if (validate(p) && !(await FileUtils.fileExists(p))) {
136-
return cloudReader.read(p)
137-
}
104+
if (result.files.length > 1) {
105+
throw new MultipleFilesError(result);
106+
}
138107

139-
return fileReader.read(p)
140-
}
141-
))
108+
return result.files[0];
142109
}
143110

144-
private parseContents(files: InMemoryFile[], sourceMaps: SourceMapCache): ParsedConfig[] {
145-
return files.flatMap((file) => {
146-
const parser = this.languageSpecificParsers[file.fileType];
147-
if (!parser) {
148-
throw new InternalError(`Unable to find a language specific parser for type ${file.fileType} for file ${file.path}`)
149-
}
111+
private parseContents(file: InMemoryFile, sourceMaps: SourceMapCache): ParseResult {
112+
const parser = this.languageSpecificParsers[file.fileType];
113+
if (!parser) {
114+
throw new InternalError(`Unable to find a language specific parser for type ${file.fileType} for file ${file.path}`)
115+
}
150116

151-
return parser.parse(file, sourceMaps);
152-
});
117+
const configs = parser.parse(file, sourceMaps);
118+
return { configs, file };
153119
}
154120

155-
private createConfigBlocks(parsedConfig: ParsedConfig[], sourceMaps: SourceMapCache): ConfigBlock[] {
156-
return parsedConfig.map((config) => ConfigFactory.create(config, sourceMaps))
121+
private createConfigBlocks(parseResult: ParseResult, sourceMaps: SourceMapCache): ConfigResult {
122+
const configs = parseResult.configs.map((config) => ConfigFactory.create(config, sourceMaps));
123+
return { configs, file: parseResult.file };
157124
}
158125
}
159126

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { DashboardApiClient } from '../../api/dashboard/index.js';
2+
import { FileType, InMemoryFile } from '../entities.js';
3+
import { Reader } from './index.js';
4+
5+
export class RemoteDocumentIdReader implements Reader {
6+
async read(filePath: string): Promise<InMemoryFile> {
7+
const document = await DashboardApiClient.getDocument(filePath);
8+
9+
return {
10+
contents: JSON.stringify(document.contents),
11+
path: filePath,
12+
fileType: FileType.REMOTE,
13+
}
14+
}
15+
16+
}

0 commit comments

Comments
 (0)