@loancrate/json-selector 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ import deepEqual from "fast-deep-equal";
2
+ import { JsonSelector, JsonSelectorCompareOperator } from "./ast";
3
+ import { findId, getField, getIndex, isArray, isFalseOrEmpty } from "./util";
4
+ import { visitJsonSelector } from "./visitor";
5
+
6
+ export function evaluateJsonSelector(
7
+ selector: JsonSelector,
8
+ context: unknown
9
+ ): unknown {
10
+ return visitJsonSelector<unknown, unknown>(
11
+ selector,
12
+ {
13
+ current() {
14
+ return context;
15
+ },
16
+ literal({ value }) {
17
+ return value;
18
+ },
19
+ identifier({ id }) {
20
+ return getField(context, id);
21
+ },
22
+ fieldAccess({ expression, field }) {
23
+ return getField(evaluateJsonSelector(expression, context), field);
24
+ },
25
+ indexAccess({ expression, index }) {
26
+ return getIndex(evaluateJsonSelector(expression, context), index);
27
+ },
28
+ idAccess({ expression, id }) {
29
+ return findId(evaluateJsonSelector(expression, context), id);
30
+ },
31
+ project({ expression, projection }) {
32
+ return project(evaluateJsonSelector(expression, context), projection);
33
+ },
34
+ filter({ expression, condition }) {
35
+ return filter(evaluateJsonSelector(expression, context), condition);
36
+ },
37
+ slice({ expression, start, end, step }) {
38
+ return slice(
39
+ evaluateJsonSelector(expression, context),
40
+ start,
41
+ end,
42
+ step
43
+ );
44
+ },
45
+ flatten({ expression }) {
46
+ return flatten(evaluateJsonSelector(expression, context));
47
+ },
48
+ not({ expression }) {
49
+ return isFalseOrEmpty(evaluateJsonSelector(expression, context));
50
+ },
51
+ compare({ lhs, rhs, operator }) {
52
+ const lv = evaluateJsonSelector(lhs, context);
53
+ const rv = evaluateJsonSelector(rhs, context);
54
+ return compare(lv, rv, operator);
55
+ },
56
+ and({ lhs, rhs }) {
57
+ const lv = evaluateJsonSelector(lhs, context);
58
+ return isFalseOrEmpty(lv) ? lv : evaluateJsonSelector(rhs, context);
59
+ },
60
+ or({ lhs, rhs }) {
61
+ const lv = evaluateJsonSelector(lhs, context);
62
+ return !isFalseOrEmpty(lv) ? lv : evaluateJsonSelector(rhs, context);
63
+ },
64
+ pipe({ lhs, rhs }) {
65
+ return evaluateJsonSelector(rhs, evaluateJsonSelector(lhs, context));
66
+ },
67
+ },
68
+ context
69
+ );
70
+ }
71
+
72
+ export function project(
73
+ value: unknown[],
74
+ projection: JsonSelector | undefined
75
+ ): unknown[];
76
+ export function project(
77
+ value: unknown,
78
+ projection: JsonSelector | undefined
79
+ ): unknown[] | null;
80
+ export function project(
81
+ value: unknown,
82
+ projection: JsonSelector | undefined
83
+ ): unknown[] | null {
84
+ if (!isArray(value)) {
85
+ return null;
86
+ }
87
+ if (!projection) {
88
+ return value;
89
+ }
90
+ const result = value
91
+ .map((e) => evaluateJsonSelector(projection, e))
92
+ .filter((e) => e != null);
93
+ return result;
94
+ }
95
+
96
+ export function filter(value: unknown[], condition: JsonSelector): unknown[];
97
+ export function filter(
98
+ value: unknown,
99
+ condition: JsonSelector
100
+ ): unknown[] | null;
101
+ export function filter(
102
+ value: unknown,
103
+ condition: JsonSelector
104
+ ): unknown[] | null {
105
+ if (!isArray(value)) {
106
+ return null;
107
+ }
108
+ const result = value.filter(
109
+ (e) => !isFalseOrEmpty(evaluateJsonSelector(condition, e))
110
+ );
111
+ return result;
112
+ }
113
+
114
+ export function slice(
115
+ value: unknown[],
116
+ start: number | undefined,
117
+ end?: number,
118
+ step?: number
119
+ ): unknown[];
120
+ export function slice(
121
+ value: unknown,
122
+ start: number | undefined,
123
+ end?: number,
124
+ step?: number
125
+ ): unknown[] | null;
126
+ export function slice(
127
+ value: unknown,
128
+ start: number | undefined,
129
+ end?: number,
130
+ step?: number
131
+ ): unknown[] | null {
132
+ if (!isArray(value)) {
133
+ return null;
134
+ }
135
+ ({ start, end, step } = normalizeSlice(value.length, start, end, step));
136
+ const collected: unknown[] = [];
137
+ if (step > 0) {
138
+ for (let i = start; i < end; i += step) {
139
+ collected.push(value[i]);
140
+ }
141
+ } else {
142
+ for (let i = start; i > end; i += step) {
143
+ collected.push(value[i]);
144
+ }
145
+ }
146
+ return collected;
147
+ }
148
+
149
+ export function normalizeSlice(
150
+ length: number,
151
+ start?: number,
152
+ end?: number,
153
+ step?: number
154
+ ): { start: number; end: number; step: number } {
155
+ if (step == null) {
156
+ step = 1;
157
+ } else if (step === 0) {
158
+ throw new Error("Invalid slice: step cannot be 0");
159
+ }
160
+ if (start == null) {
161
+ start = step < 0 ? length - 1 : 0;
162
+ } else {
163
+ start = limitSlice(start, step, length);
164
+ }
165
+ if (end == null) {
166
+ end = step < 0 ? -1 : length;
167
+ } else {
168
+ end = limitSlice(end, step, length);
169
+ }
170
+ return { start, end, step };
171
+ }
172
+
173
+ function limitSlice(value: number, step: number, length: number): number {
174
+ if (value < 0) {
175
+ value += length;
176
+ if (value < 0) {
177
+ value = step < 0 ? -1 : 0;
178
+ }
179
+ } else if (value >= length) {
180
+ value = step < 0 ? length - 1 : length;
181
+ }
182
+ return value;
183
+ }
184
+
185
+ export function flatten(value: unknown[]): unknown[];
186
+ export function flatten(value: unknown): unknown[] | null;
187
+ export function flatten(value: unknown): unknown[] | null {
188
+ return isArray(value) ? value.flat() : null;
189
+ }
190
+
191
+ export function compare(
192
+ lv: number,
193
+ rv: number,
194
+ operator: JsonSelectorCompareOperator
195
+ ): boolean;
196
+ export function compare(
197
+ lv: unknown,
198
+ rv: unknown,
199
+ operator: JsonSelectorCompareOperator
200
+ ): boolean | null;
201
+ export function compare(
202
+ lv: unknown,
203
+ rv: unknown,
204
+ operator: JsonSelectorCompareOperator
205
+ ): boolean | null {
206
+ switch (operator) {
207
+ case "==":
208
+ return deepEqual(lv, rv);
209
+ case "!=":
210
+ return !deepEqual(lv, rv);
211
+ case "<":
212
+ case "<=":
213
+ case ">":
214
+ case ">=":
215
+ if (typeof lv === "number" && typeof rv === "number") {
216
+ switch (operator) {
217
+ case "<":
218
+ return lv < rv;
219
+ case "<=":
220
+ return lv <= rv;
221
+ case ">":
222
+ return lv > rv;
223
+ case ">=":
224
+ return lv >= rv;
225
+ }
226
+ }
227
+ }
228
+ return null;
229
+ }
package/src/format.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { JsonSelector, JsonSelectorNodeType } from "./ast";
2
+ import { formatIdentifier, formatLiteral, formatRawString } from "./util";
3
+ import { visitJsonSelector } from "./visitor";
4
+
5
+ const PRECEDENCE_ACCESS = 1;
6
+ const PRECEDENCE_NOT = 2;
7
+ const PRECEDENCE_COMPARE = 3;
8
+ const PRECEDENCE_AND = 4;
9
+ const PRECEDENCE_OR = 5;
10
+ const PRECEDENCE_PIPE = 6;
11
+ const PRECEDENCE_MAX = 7;
12
+
13
+ const operatorPrecedence: { [type in JsonSelectorNodeType]?: number } = {
14
+ fieldAccess: PRECEDENCE_ACCESS,
15
+ idAccess: PRECEDENCE_ACCESS,
16
+ indexAccess: PRECEDENCE_ACCESS,
17
+ project: PRECEDENCE_ACCESS,
18
+ filter: PRECEDENCE_ACCESS,
19
+ slice: PRECEDENCE_ACCESS,
20
+ flatten: PRECEDENCE_ACCESS,
21
+ not: PRECEDENCE_NOT,
22
+ compare: PRECEDENCE_COMPARE,
23
+ and: PRECEDENCE_AND,
24
+ or: PRECEDENCE_OR,
25
+ pipe: PRECEDENCE_PIPE,
26
+ };
27
+
28
+ function formatSubexpression(
29
+ expr: JsonSelector,
30
+ options: Partial<FormatJsonSelectorOptions>,
31
+ precedence: number
32
+ ): string {
33
+ // Ensure @ is only used as a bare expression
34
+ if (!options.currentImplied) {
35
+ options = { ...options, currentImplied: true };
36
+ }
37
+ let result = formatJsonSelector(expr, options);
38
+ const subPrecedence = operatorPrecedence[expr.type];
39
+ if (subPrecedence != null && subPrecedence > precedence) {
40
+ result = `(${result})`;
41
+ }
42
+ return result;
43
+ }
44
+
45
+ const projectionNodeTypes = new Set<JsonSelectorNodeType>([
46
+ "project",
47
+ "filter",
48
+ "slice",
49
+ "flatten",
50
+ ]);
51
+
52
+ export interface FormatJsonSelectorOptions {
53
+ currentImplied: boolean;
54
+ }
55
+
56
+ export function formatJsonSelector(
57
+ selector: JsonSelector,
58
+ options: Partial<FormatJsonSelectorOptions> = {}
59
+ ): string {
60
+ return visitJsonSelector<string, undefined>(
61
+ selector,
62
+ {
63
+ current() {
64
+ return !options.currentImplied ? "@" : "";
65
+ },
66
+ literal({ value }) {
67
+ return formatLiteral(value);
68
+ },
69
+ identifier({ id }) {
70
+ return formatIdentifier(id);
71
+ },
72
+ fieldAccess({ expression, field }) {
73
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
74
+ return `${lv}.${formatIdentifier(field)}`;
75
+ },
76
+ indexAccess({ expression, index }) {
77
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
78
+ return `${lv}[${index}]`;
79
+ },
80
+ idAccess({ expression, id }) {
81
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
82
+ return `${lv}[${formatRawString(id)}]`;
83
+ },
84
+ project({ expression, projection }) {
85
+ let result = formatSubexpression(
86
+ expression,
87
+ options,
88
+ PRECEDENCE_ACCESS
89
+ );
90
+ // Wildcard operator is only needed if expression is not already a projection
91
+ if (!projectionNodeTypes.has(expression.type)) {
92
+ result += "[*]";
93
+ }
94
+ if (projection) {
95
+ result += formatSubexpression(projection, options, PRECEDENCE_MAX);
96
+ }
97
+ return result;
98
+ },
99
+ filter({ expression, condition }) {
100
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
101
+ const rv = formatSubexpression(condition, options, PRECEDENCE_MAX);
102
+ return `${lv}[?${rv}]`;
103
+ },
104
+ slice({ expression, start, end, step }) {
105
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
106
+ const rv = `${start ?? ""}:${end ?? ""}${
107
+ step != null ? `:${step}` : ""
108
+ }`;
109
+ return `${lv}[${rv}]`;
110
+ },
111
+ flatten({ expression }) {
112
+ const lv = formatSubexpression(expression, options, PRECEDENCE_ACCESS);
113
+ return `${lv}[]`;
114
+ },
115
+ not({ expression }) {
116
+ return `!${formatSubexpression(expression, options, PRECEDENCE_NOT)}`;
117
+ },
118
+ compare({ lhs, operator, rhs }) {
119
+ const lv = formatSubexpression(lhs, options, PRECEDENCE_COMPARE);
120
+ const rv = formatSubexpression(rhs, options, PRECEDENCE_COMPARE - 1);
121
+ return `${lv} ${operator} ${rv}`;
122
+ },
123
+ and({ lhs, rhs }) {
124
+ const lv = formatSubexpression(lhs, options, PRECEDENCE_AND);
125
+ const rv = formatSubexpression(rhs, options, PRECEDENCE_AND - 1);
126
+ return `${lv} && ${rv}`;
127
+ },
128
+ or({ lhs, rhs }) {
129
+ const lv = formatSubexpression(lhs, options, PRECEDENCE_OR);
130
+ const rv = formatSubexpression(rhs, options, PRECEDENCE_OR - 1);
131
+ return `${lv} || ${rv}`;
132
+ },
133
+ pipe({ lhs, rhs }) {
134
+ const lv = formatSubexpression(lhs, options, PRECEDENCE_PIPE);
135
+ const rv = formatSubexpression(rhs, options, PRECEDENCE_PIPE - 1);
136
+ return `${lv} | ${rv}`;
137
+ },
138
+ },
139
+ undefined
140
+ );
141
+ }
package/src/get.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { accessWithJsonSelector } from "./access";
2
+ import { JsonSelector } from "./ast";
3
+
4
+ export function getWithJsonSelector(
5
+ selector: JsonSelector,
6
+ context: unknown
7
+ ): unknown {
8
+ return accessWithJsonSelector(selector, context).get();
9
+ }
@@ -0,0 +1,217 @@
1
+ // Based on https://jmespath.org/specification.html#grammar
2
+
3
+ {
4
+ function binaryExpression(type, head, tail) {
5
+ return tail.reduce((lhs, rhs) => (
6
+ {
7
+ type,
8
+ lhs,
9
+ rhs
10
+ }
11
+ ), head);
12
+ }
13
+
14
+ function reduceProjection(lhs, rhs) {
15
+ return rhs.reduce((result, fn) => fn(result), lhs);
16
+ }
17
+
18
+ function maybeProject(expression, pfns) {
19
+ return !pfns.length ? expression : (
20
+ {
21
+ type: "project",
22
+ expression: unwrapTrivialProjection(expression),
23
+ projection: pfns.reduce((result, pfn) => pfn(result), { type: "current" })
24
+ });
25
+ }
26
+
27
+ function unwrapTrivialProjection(expression) {
28
+ const { type, projection } = expression;
29
+ return type === "project" && (!projection || projection.type === "current") ? expression.expression : expression;
30
+ }
31
+ }
32
+
33
+ selector = ws @pipe_expression ws
34
+
35
+ pipe_expression = head:or_expression tail:(ws "|" ws @or_expression)*
36
+ { return binaryExpression("pipe", head, tail); }
37
+
38
+ or_expression = head:and_expression tail:(ws "||" ws @and_expression)*
39
+ { return binaryExpression("or", head, tail); }
40
+
41
+ and_expression = head:compare_expression tail:(ws "&&" ws @compare_expression)*
42
+ { return binaryExpression("and", head, tail); }
43
+
44
+ compare_expression = head:not_expression tail:(ws @("<=" / ">=" / "<" / ">" / "==" / "!=") ws @not_expression)*
45
+ {
46
+ return tail.reduce((result, [operator, rhs]) => (
47
+ {
48
+ type: "compare",
49
+ operator,
50
+ lhs: result,
51
+ rhs
52
+ }
53
+ ), head);
54
+ }
55
+
56
+ not_expression =
57
+ "!" expression:not_expression { return { type: "not", expression }; }
58
+ / flatten_expression
59
+
60
+ flatten_expression = lhs:flatten_lhs rhs:flatten_rhs* { return reduceProjection(lhs, rhs); }
61
+
62
+ flatten_lhs =
63
+ flatten:flatten { return flatten({ type: "current" }); }
64
+ / filter_expression
65
+
66
+ flatten_rhs =
67
+ flatten:flatten pfns:filter_rhs* { return (expression) => maybeProject(flatten(expression), pfns); }
68
+ / filter_rhs
69
+
70
+ flatten = ws "[]" { return (expression) => ({ type: "flatten", expression }); }
71
+
72
+ filter_expression = lhs:filter_lhs rhs:filter_rhs* { return reduceProjection(lhs, rhs); }
73
+
74
+ filter_lhs =
75
+ filter:filter { return filter({ type: "current" }); }
76
+ / projection_expression
77
+
78
+ filter_rhs =
79
+ filter:filter pfns:filter_rhs* { return (expression) => maybeProject(filter(expression), pfns); }
80
+ / projection_rhs
81
+ / dot_rhs
82
+
83
+ filter = ws "[?" condition:selector "]" { return (expression) => ({ type: "filter", expression, condition }); }
84
+
85
+ projection_expression = lhs:projection_lhs rhs:projection_rhs* { return reduceProjection(lhs, rhs); }
86
+
87
+ projection_lhs =
88
+ projection:projection { return projection({ type: "current" }); }
89
+ / index_expression
90
+
91
+ projection_rhs =
92
+ projection:projection pfns:filter_rhs* { return (expression) => maybeProject(projection(expression), pfns); }
93
+ / index_rhs
94
+
95
+ projection =
96
+ ws "[" ws "*" ws "]" { return (expression) => ({ type: "project", expression, projection: { type: "current" } }); }
97
+ / ws "[" ws @slice ws "]"
98
+
99
+ slice = start:number? ws ":" ws end:number? ws ":"? ws step:number?
100
+ { return (expression) => ({ type: "slice", expression, start, end, step }); }
101
+
102
+ index_expression = lhs:index_lhs rhs:index_rhs* { return reduceProjection(lhs, rhs); }
103
+
104
+ index_lhs =
105
+ index:index_rhs { return index({ type: "current" }); }
106
+ / member_expression
107
+
108
+ index_rhs =
109
+ ws "[" ws index:number ws "]" { return (expression) => ({ type: "indexAccess", expression, index }); }
110
+ / ws "[" ws id:raw_string ws "]" { return (expression) => ({ type: "idAccess", expression, id }); }
111
+
112
+ dot_rhs = ws "." ws field:identifier
113
+ { return (expression) => ({ type: "fieldAccess", expression, field }); }
114
+
115
+ member_expression = lhs:primary_expression rhs:dot_rhs* { return reduceProjection(lhs, rhs); }
116
+
117
+ primary_expression =
118
+ id:identifier { return { type: "identifier", id }; }
119
+ / "@" { return { type: "current" }; }
120
+ / literal
121
+ / value:raw_string { return { type: "literal", value }; }
122
+ / "(" @selector ")"
123
+
124
+ literal = "`" ws value:(json_value / unquoted_json_string) ws "`"
125
+ { return { type: "literal", value }; }
126
+
127
+ // Identifiers and double-quoted strings
128
+
129
+ identifier = unquoted_string / quoted_string
130
+
131
+ unquoted_string = head:[a-z_]i tail:[0-9a-z_]i* { return head + tail.join(""); }
132
+
133
+ quoted_string = '"' chars:char* '"' { return chars.join(""); }
134
+
135
+ char = unescaped_char / escaped_char
136
+
137
+ unescaped_char = [^\0-\x1F"\\]
138
+
139
+ escaped_char = "\\" @(
140
+ '"'
141
+ / "\\"
142
+ / "/"
143
+ / "b" { return "\b"; }
144
+ / "f" { return "\f"; }
145
+ / "n" { return "\n"; }
146
+ / "r" { return "\r"; }
147
+ / "t" { return "\t"; }
148
+ / "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG) {
149
+ return String.fromCharCode(parseInt(digits, 16));
150
+ }
151
+ )
152
+
153
+ HEXDIG = [0-9a-f]i
154
+
155
+ // Raw strings (single-quoted)
156
+
157
+ raw_string = "'" chars:raw_string_char* "'" { return chars.join(""); }
158
+
159
+ raw_string_char = unescaped_raw_string_char / preserved_escape / raw_string_escape
160
+
161
+ // Despite what the specification says, JMESPath implementations accept control characters
162
+ unescaped_raw_string_char = [^'\\]
163
+
164
+ preserved_escape = "\\" [^'] { return text(); }
165
+
166
+ raw_string_escape = "\\" @"'"
167
+
168
+ // Numbers
169
+
170
+ number = int { return parseInt(text()); }
171
+
172
+ int = "-"? ("0" / ([1-9] [0-9]*))
173
+
174
+ // JSON
175
+
176
+ json_value =
177
+ "null" { return null; }
178
+ / "false" { return false; }
179
+ / "true" { return true; }
180
+ / json_number
181
+ / json_string
182
+ / json_object
183
+ / json_array
184
+
185
+ json_number = int ("." [0-9]+)? ([eE] [+-]? [0-9]+)? { return parseFloat(text()); }
186
+
187
+ json_string = '"' @unquoted_json_string '"'
188
+
189
+ unquoted_json_string = chars:(unescaped_literal / escaped_literal)* { return chars.join(""); }
190
+
191
+ unescaped_literal = [^\0-\x1F"\\`]
192
+
193
+ escaped_literal = escaped_char / "\\" @"`"
194
+
195
+ json_object =
196
+ "{" ws members:(
197
+ head:json_member
198
+ tail:(ws "," ws @json_member)*
199
+ { return [head].concat(tail); }
200
+ )?
201
+ ws "}"
202
+ { return Object.fromEntries(members ?? []); }
203
+
204
+ json_member = name:json_string ws ":" ws value:json_value
205
+ { return [name, value]; }
206
+
207
+ json_array =
208
+ "[" ws values:(
209
+ head:json_value
210
+ tail:(ws "," ws @json_value)*
211
+ { return [head].concat(tail); }
212
+ )? ws "]"
213
+ { return values ?? []; }
214
+
215
+ // Whitespace
216
+
217
+ ws "whitespace" = [ \t\n\r]*
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./access";
2
+ export * from "./ast";
3
+ export { evaluateJsonSelector } from "./evaluate";
4
+ export * from "./format";
5
+ export * from "./get";
6
+ export * from "./parse";
7
+ export * from "./set";
package/src/parse.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { JsonSelector } from "./ast";
2
+ import { parse } from "./__generated__/parser";
3
+
4
+ export function parseJsonSelector(selectorExpression: string): JsonSelector {
5
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
6
+ return parse(selectorExpression);
7
+ }
package/src/set.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { accessWithJsonSelector } from "./access";
2
+ import { JsonSelector } from "./ast";
3
+
4
+ export function setWithJsonSelector(
5
+ selector: JsonSelector,
6
+ context: unknown,
7
+ value: unknown
8
+ ): unknown {
9
+ const accessor = accessWithJsonSelector(selector, context);
10
+ const oldValue = accessor.get();
11
+ accessor.set(value);
12
+ return oldValue;
13
+ }
package/src/util.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { JsonValue } from "type-fest";
2
+
3
+ export function isArray(value: unknown): value is unknown[] {
4
+ return Array.isArray(value);
5
+ }
6
+
7
+ export function asArray(value: unknown): unknown[] {
8
+ return value == null ? [] : isArray(value) ? value : [value];
9
+ }
10
+
11
+ export function isObject(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === "object" && value != null;
13
+ }
14
+
15
+ export function isFalseOrEmpty(value: unknown): boolean {
16
+ return (
17
+ value == null ||
18
+ value === false ||
19
+ value === "" ||
20
+ (isArray(value) && value.length === 0) ||
21
+ (isObject(value) && !hasOwnProperties(value))
22
+ );
23
+ }
24
+
25
+ function hasOwnProperties(value: Record<string, unknown>): boolean {
26
+ for (const key in value) {
27
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
28
+ return true;
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ export function getField(obj: unknown, name: string): unknown {
35
+ return isObject(obj) ? obj[name] ?? null : null;
36
+ }
37
+
38
+ export function getIndex(obj: unknown, index: number): unknown {
39
+ return isArray(obj)
40
+ ? obj[index < 0 ? obj.length + index : index] ?? null
41
+ : null;
42
+ }
43
+
44
+ export function findId(obj: unknown, id: string | number): unknown {
45
+ return isArray(obj)
46
+ ? obj.find((e) => isObject(e) && e.id === id) ?? null
47
+ : null;
48
+ }
49
+
50
+ export function findIdIndex(arr: unknown[], id: string | number): number {
51
+ return arr.findIndex((e) => isObject(e) && e.id === id);
52
+ }
53
+
54
+ export function formatLiteral(value: JsonValue): string {
55
+ return "`" + JSON.stringify(value).replace(/`/g, "\\`") + "`";
56
+ }
57
+
58
+ export function isValidIdentifier(s: string): boolean {
59
+ return /^[a-z_][0-9a-z_]*$/i.test(s);
60
+ }
61
+
62
+ export function formatIdentifier(s: string): string {
63
+ return isValidIdentifier(s) ? s : JSON.stringify(s);
64
+ }
65
+
66
+ export function formatRawString(s: string): string {
67
+ // https://jmespath.org/specification.html#raw-string-literals
68
+ // eslint-disable-next-line no-control-regex
69
+ return `'${s.replace(/[\0-\x1F]/g, "").replace(/['\\]/g, (c) => `\\${c}`)}'`;
70
+ }