-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathJSONCTweaker.ts
156 lines (141 loc) · 4.63 KB
/
JSONCTweaker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { applyEdits, modify, parse, type Segment } from 'jsonc-parser';
import type { ParseError } from 'jsonc-parser';
import { DefaultFormattingOptions } from './DefaultFormattingOptions.ts';
import { getAlphabeticalInsertionIndex } from './getAlphabeticalInsertionIndex.ts';
import { getValueAtPath } from './getValueAtPath.ts';
export interface InsertArrayValueOptions
{
/**
* If true, the value will only be inserted if it doesn't already exist in the array
*/
skipIfExists?: boolean;
}
/**
A convenience class for working with JSONC files — based on the Microsoft JSONC parser (https://github.com/microsoft/node-jsonc-parser) but more convenient to use for certain specific tasks, like "insert string "ho ho ho" into the array at `hoge.hige.hage` if it doesn't already exist".
*/
export class JSONCTweaker
{
/**
Initialize a new tweaker from the given text
*/
static fromText(input: string): JSONCTweaker
{
return new JSONCTweaker(input);
}
/**
Initialize a new tweaker from the given file — this means every change will write back to the file. There are no coalesced writes.
*/
static fromFile(input: string): JSONCTweaker
{
return new JSONCTweaker(input, true);
}
private _jsonc: string;
private _filePath?: string;
/**
Returns the current JSONC text
*/
get jsonc(): string
{
return this._jsonc;
}
/**
Initialize a new tweaker from the given text or file. Note that there are no coalesced writes, so every successful edit will write back to the file. (If you need granular control of filesystem writees, then use the text mode and deal with the file yourself.)
*/
constructor(input: string, isFilePath = false)
{
if (isFilePath)
{
this._filePath = input;
this._jsonc = Deno.readTextFileSync(input);
}
else
{
this._jsonc = input;
}
}
private async _writeIfFile(): Promise<void>
{
if (this._filePath)
{
await Deno.writeTextFile(this._filePath, this._jsonc);
}
}
/**
Insert the given value into the array at the given path. If the path does not exist in the receiver's JSONC, a new array will be created. Otherwise, the value will be inserted into the existing array.
Returns true if the edit succeeded, false otherwise. You can inspect the `outErrors` parameter to find out why the edit failed (maybe) — it may contain errors from the underlying Microsoft JSONC parser, or errors from this module.
*/
async insertArrayValue(
pathToArray: Segment[],
value: string,
outErrors?: Array<ParseError | Error>,
options: InsertArrayValueOptions = {},
): Promise<boolean>
{
const parseErrors: ParseError[] = [];
const parsed = parse(this._jsonc, parseErrors);
const existing = getValueAtPath(parsed, pathToArray) as string[] | undefined;
if (existing === undefined)
{
const err = new Error(`Path does not exist: ${pathToArray.join('.')}`);
if (outErrors)
{
outErrors.push(err);
}
return false;
}
if (!Array.isArray(existing))
{
const err = new Error(`Path is not an array: ${pathToArray.join('.')}`);
if (outErrors)
{
outErrors.push(err);
}
return false;
}
if (options.skipIfExists && existing.includes(value))
{
return true;
}
const insertionIndex = getAlphabeticalInsertionIndex(value, existing);
try
{
const modifyResult = modify(this._jsonc, [...pathToArray, insertionIndex], value, {
formattingOptions: DefaultFormattingOptions,
isArrayInsertion: true,
});
this._jsonc = applyEdits(this._jsonc, modifyResult);
await this._writeIfFile();
return true;
}
catch (error)
{
if (outErrors)
{
outErrors.push(
error instanceof Error ? error : new Error(`Edit operation failed for ${pathToArray.join('.')}`),
);
}
return false;
}
}
/**
Insert the given value into the array at the given path only if it doesn't already exist. This is a convenience wrapper around insertArrayValue with skipIfExists set to true.
*/
async insertArrayValueIfNotPresent(
pathToArray: Segment[],
value: string,
outErrors?: Array<ParseError | Error>,
): Promise<boolean>
{
return await this.insertArrayValue(pathToArray, value, outErrors, { skipIfExists: true });
}
/**
Get the value at the specified path in the JSONC document. Returns undefined if the path doesn't lead to a value.
*/
getValue(path: Segment[]): unknown
{
const parseErrors: ParseError[] = [];
const parsed = parse(this._jsonc, parseErrors);
return getValueAtPath(parsed, path);
}
}