@ai-sdk/google 3.0.60 → 3.0.61
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.
- package/CHANGELOG.md +6 -0
- package/dist/index.d.mts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +369 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -54
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.d.mts +11 -2
- package/dist/internal/index.d.ts +11 -2
- package/dist/internal/index.js +368 -53
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +368 -53
- package/dist/internal/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/google-generative-ai-language-model.ts +193 -29
- package/src/google-generative-ai-options.ts +11 -0
- package/src/google-json-accumulator.ts +336 -0
- package/src/google-prepare-tools.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/google",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.61",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"tsup": "^8",
|
|
45
45
|
"typescript": "5.8.3",
|
|
46
46
|
"zod": "3.25.76",
|
|
47
|
-
"@ai-
|
|
48
|
-
"@
|
|
47
|
+
"@vercel/ai-tsconfig": "0.0.0",
|
|
48
|
+
"@ai-sdk/test-server": "1.0.3"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"zod": "^3.25.76 || ^4.1.8"
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
GoogleGenerativeAIProviderMetadata,
|
|
45
45
|
} from './google-generative-ai-prompt';
|
|
46
46
|
import { prepareTools } from './google-prepare-tools';
|
|
47
|
+
import { GoogleJSONAccumulator, PartialArg } from './google-json-accumulator';
|
|
47
48
|
import { mapGoogleGenerativeAIFinishReason } from './map-google-generative-ai-finish-reason';
|
|
48
49
|
|
|
49
50
|
type GoogleGenerativeAIConfig = {
|
|
@@ -119,12 +120,14 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// Add warning if Vertex rag tools are used with a non-Vertex Google provider
|
|
123
|
+
const isVertexProvider = this.config.provider.startsWith('google.vertex.');
|
|
124
|
+
|
|
122
125
|
if (
|
|
123
126
|
tools?.some(
|
|
124
127
|
tool =>
|
|
125
128
|
tool.type === 'provider' && tool.id === 'google.vertex_rag_store',
|
|
126
129
|
) &&
|
|
127
|
-
!
|
|
130
|
+
!isVertexProvider
|
|
128
131
|
) {
|
|
129
132
|
warnings.push({
|
|
130
133
|
type: 'other',
|
|
@@ -135,6 +138,16 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
135
138
|
});
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
if (googleOptions?.streamFunctionCallArguments && !isVertexProvider) {
|
|
142
|
+
warnings.push({
|
|
143
|
+
type: 'other',
|
|
144
|
+
message:
|
|
145
|
+
"'streamFunctionCallArguments' is only supported on the Vertex AI API " +
|
|
146
|
+
'and will be ignored with the current Google provider ' +
|
|
147
|
+
`(${this.config.provider}). See https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
const isGemmaModel = this.modelId.toLowerCase().startsWith('gemma-');
|
|
139
152
|
const supportsFunctionResponseParts = this.modelId.startsWith('gemini-3');
|
|
140
153
|
|
|
@@ -157,6 +170,28 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
157
170
|
modelId: this.modelId,
|
|
158
171
|
});
|
|
159
172
|
|
|
173
|
+
const streamFunctionCallArguments = isVertexProvider
|
|
174
|
+
? (googleOptions?.streamFunctionCallArguments ?? true)
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
const toolConfig =
|
|
178
|
+
googleToolConfig ||
|
|
179
|
+
streamFunctionCallArguments ||
|
|
180
|
+
googleOptions?.retrievalConfig
|
|
181
|
+
? {
|
|
182
|
+
...googleToolConfig,
|
|
183
|
+
...(streamFunctionCallArguments && {
|
|
184
|
+
functionCallingConfig: {
|
|
185
|
+
...googleToolConfig?.functionCallingConfig,
|
|
186
|
+
streamFunctionCallArguments: true as const,
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
...(googleOptions?.retrievalConfig && {
|
|
190
|
+
retrievalConfig: googleOptions.retrievalConfig,
|
|
191
|
+
}),
|
|
192
|
+
}
|
|
193
|
+
: undefined;
|
|
194
|
+
|
|
160
195
|
return {
|
|
161
196
|
args: {
|
|
162
197
|
generationConfig: {
|
|
@@ -200,12 +235,7 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
200
235
|
systemInstruction: isGemmaModel ? undefined : systemInstruction,
|
|
201
236
|
safetySettings: googleOptions?.safetySettings,
|
|
202
237
|
tools: googleTools,
|
|
203
|
-
toolConfig
|
|
204
|
-
? {
|
|
205
|
-
...googleToolConfig,
|
|
206
|
-
retrievalConfig: googleOptions.retrievalConfig,
|
|
207
|
-
}
|
|
208
|
-
: googleToolConfig,
|
|
238
|
+
toolConfig,
|
|
209
239
|
cachedContent: googleOptions?.cachedContent,
|
|
210
240
|
labels: googleOptions?.labels,
|
|
211
241
|
serviceTier: googleOptions?.serviceTier,
|
|
@@ -301,7 +331,11 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
301
331
|
providerMetadata: thoughtSignatureMetadata,
|
|
302
332
|
});
|
|
303
333
|
}
|
|
304
|
-
} else if (
|
|
334
|
+
} else if (
|
|
335
|
+
'functionCall' in part &&
|
|
336
|
+
part.functionCall.name != null &&
|
|
337
|
+
part.functionCall.args != null
|
|
338
|
+
) {
|
|
305
339
|
content.push({
|
|
306
340
|
type: 'tool-call' as const,
|
|
307
341
|
toolCallId: this.config.generateId(),
|
|
@@ -478,6 +512,13 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
478
512
|
// Associates a server-side tool response with its preceding call (tool combination).
|
|
479
513
|
let lastServerToolCallId: string | undefined;
|
|
480
514
|
|
|
515
|
+
const activeStreamingToolCalls: Array<{
|
|
516
|
+
toolCallId: string;
|
|
517
|
+
toolName: string;
|
|
518
|
+
accumulator: GoogleJSONAccumulator;
|
|
519
|
+
providerMetadata?: SharedV3ProviderMetadata;
|
|
520
|
+
}> = [];
|
|
521
|
+
|
|
481
522
|
return {
|
|
482
523
|
stream: response.pipeThrough(
|
|
483
524
|
new TransformStream<
|
|
@@ -735,40 +776,152 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
|
|
|
735
776
|
}
|
|
736
777
|
}
|
|
737
778
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
779
|
+
// Handle streaming and complete function calls
|
|
780
|
+
for (const part of parts) {
|
|
781
|
+
if (!('functionCall' in part)) continue;
|
|
782
|
+
|
|
783
|
+
const providerMeta = part.thoughtSignature
|
|
784
|
+
? {
|
|
785
|
+
[providerOptionsName]: {
|
|
786
|
+
thoughtSignature: part.thoughtSignature,
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
: undefined;
|
|
790
|
+
|
|
791
|
+
const isStreamingChunk =
|
|
792
|
+
part.functionCall.partialArgs != null ||
|
|
793
|
+
(part.functionCall.name != null &&
|
|
794
|
+
part.functionCall.willContinue === true);
|
|
795
|
+
const isTerminalChunk =
|
|
796
|
+
part.functionCall.name == null &&
|
|
797
|
+
part.functionCall.args == null &&
|
|
798
|
+
part.functionCall.partialArgs == null &&
|
|
799
|
+
part.functionCall.willContinue == null;
|
|
800
|
+
const isCompleteCall =
|
|
801
|
+
part.functionCall.name != null &&
|
|
802
|
+
part.functionCall.args != null &&
|
|
803
|
+
part.functionCall.partialArgs == null;
|
|
804
|
+
|
|
805
|
+
if (isStreamingChunk) {
|
|
806
|
+
if (
|
|
807
|
+
part.functionCall.name != null &&
|
|
808
|
+
part.functionCall.willContinue === true
|
|
809
|
+
) {
|
|
810
|
+
const toolCallId = generateId();
|
|
811
|
+
const accumulator = new GoogleJSONAccumulator();
|
|
812
|
+
activeStreamingToolCalls.push({
|
|
813
|
+
toolCallId,
|
|
814
|
+
toolName: part.functionCall.name,
|
|
815
|
+
accumulator,
|
|
816
|
+
providerMetadata: providerMeta,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
controller.enqueue({
|
|
820
|
+
type: 'tool-input-start',
|
|
821
|
+
id: toolCallId,
|
|
822
|
+
toolName: part.functionCall.name,
|
|
823
|
+
providerMetadata: providerMeta,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
if (part.functionCall.partialArgs != null) {
|
|
827
|
+
const { textDelta } = accumulator.processPartialArgs(
|
|
828
|
+
part.functionCall.partialArgs as PartialArg[],
|
|
829
|
+
);
|
|
830
|
+
if (textDelta.length > 0) {
|
|
831
|
+
controller.enqueue({
|
|
832
|
+
type: 'tool-input-delta',
|
|
833
|
+
id: toolCallId,
|
|
834
|
+
delta: textDelta,
|
|
835
|
+
providerMetadata: providerMeta,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} else if (
|
|
840
|
+
part.functionCall.partialArgs != null &&
|
|
841
|
+
activeStreamingToolCalls.length > 0
|
|
842
|
+
) {
|
|
843
|
+
const active =
|
|
844
|
+
activeStreamingToolCalls[
|
|
845
|
+
activeStreamingToolCalls.length - 1
|
|
846
|
+
];
|
|
847
|
+
const { textDelta } = active.accumulator.processPartialArgs(
|
|
848
|
+
part.functionCall.partialArgs as PartialArg[],
|
|
849
|
+
);
|
|
850
|
+
if (textDelta.length > 0) {
|
|
851
|
+
controller.enqueue({
|
|
852
|
+
type: 'tool-input-delta',
|
|
853
|
+
id: active.toolCallId,
|
|
854
|
+
delta: textDelta,
|
|
855
|
+
providerMetadata: providerMeta,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
} else if (
|
|
860
|
+
isTerminalChunk &&
|
|
861
|
+
activeStreamingToolCalls.length > 0
|
|
862
|
+
) {
|
|
863
|
+
const active = activeStreamingToolCalls.pop()!;
|
|
864
|
+
const { finalJSON, closingDelta } =
|
|
865
|
+
active.accumulator.finalize();
|
|
866
|
+
|
|
867
|
+
if (closingDelta.length > 0) {
|
|
868
|
+
controller.enqueue({
|
|
869
|
+
type: 'tool-input-delta',
|
|
870
|
+
id: active.toolCallId,
|
|
871
|
+
delta: closingDelta,
|
|
872
|
+
providerMetadata: active.providerMetadata,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
controller.enqueue({
|
|
877
|
+
type: 'tool-input-end',
|
|
878
|
+
id: active.toolCallId,
|
|
879
|
+
providerMetadata: active.providerMetadata,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
controller.enqueue({
|
|
883
|
+
type: 'tool-call',
|
|
884
|
+
toolCallId: active.toolCallId,
|
|
885
|
+
toolName: active.toolName,
|
|
886
|
+
input: finalJSON,
|
|
887
|
+
providerMetadata: active.providerMetadata,
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
hasToolCalls = true;
|
|
891
|
+
} else if (isCompleteCall) {
|
|
892
|
+
const toolCallId = generateId();
|
|
893
|
+
const toolName = part.functionCall.name!;
|
|
894
|
+
const args =
|
|
895
|
+
typeof part.functionCall.args === 'string'
|
|
896
|
+
? part.functionCall.args
|
|
897
|
+
: JSON.stringify(part.functionCall.args ?? {});
|
|
743
898
|
|
|
744
|
-
if (toolCallDeltas != null) {
|
|
745
|
-
for (const toolCall of toolCallDeltas) {
|
|
746
899
|
controller.enqueue({
|
|
747
900
|
type: 'tool-input-start',
|
|
748
|
-
id:
|
|
749
|
-
toolName
|
|
750
|
-
providerMetadata:
|
|
901
|
+
id: toolCallId,
|
|
902
|
+
toolName,
|
|
903
|
+
providerMetadata: providerMeta,
|
|
751
904
|
});
|
|
752
905
|
|
|
753
906
|
controller.enqueue({
|
|
754
907
|
type: 'tool-input-delta',
|
|
755
|
-
id:
|
|
756
|
-
delta:
|
|
757
|
-
providerMetadata:
|
|
908
|
+
id: toolCallId,
|
|
909
|
+
delta: args,
|
|
910
|
+
providerMetadata: providerMeta,
|
|
758
911
|
});
|
|
759
912
|
|
|
760
913
|
controller.enqueue({
|
|
761
914
|
type: 'tool-input-end',
|
|
762
|
-
id:
|
|
763
|
-
providerMetadata:
|
|
915
|
+
id: toolCallId,
|
|
916
|
+
providerMetadata: providerMeta,
|
|
764
917
|
});
|
|
765
918
|
|
|
766
919
|
controller.enqueue({
|
|
767
920
|
type: 'tool-call',
|
|
768
|
-
toolCallId
|
|
769
|
-
toolName
|
|
770
|
-
input:
|
|
771
|
-
providerMetadata:
|
|
921
|
+
toolCallId,
|
|
922
|
+
toolName,
|
|
923
|
+
input: args,
|
|
924
|
+
providerMetadata: providerMeta,
|
|
772
925
|
});
|
|
773
926
|
|
|
774
927
|
hasToolCalls = true;
|
|
@@ -1040,6 +1193,15 @@ export const getGroundingMetadataSchema = () =>
|
|
|
1040
1193
|
.nullish(),
|
|
1041
1194
|
});
|
|
1042
1195
|
|
|
1196
|
+
const partialArgSchema = z.object({
|
|
1197
|
+
jsonPath: z.string(),
|
|
1198
|
+
stringValue: z.string().nullish(),
|
|
1199
|
+
numberValue: z.number().nullish(),
|
|
1200
|
+
boolValue: z.boolean().nullish(),
|
|
1201
|
+
nullValue: z.unknown().nullish(),
|
|
1202
|
+
willContinue: z.boolean().nullish(),
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1043
1205
|
const getContentSchema = () =>
|
|
1044
1206
|
z.object({
|
|
1045
1207
|
parts: z
|
|
@@ -1048,8 +1210,10 @@ const getContentSchema = () =>
|
|
|
1048
1210
|
// note: order matters since text can be fully empty
|
|
1049
1211
|
z.object({
|
|
1050
1212
|
functionCall: z.object({
|
|
1051
|
-
name: z.string(),
|
|
1052
|
-
args: z.unknown(),
|
|
1213
|
+
name: z.string().nullish(),
|
|
1214
|
+
args: z.unknown().nullish(),
|
|
1215
|
+
partialArgs: z.array(partialArgSchema).nullish(),
|
|
1216
|
+
willContinue: z.boolean().nullish(),
|
|
1053
1217
|
}),
|
|
1054
1218
|
thoughtSignature: z.string().nullish(),
|
|
1055
1219
|
}),
|
|
@@ -189,6 +189,17 @@ export const googleLanguageModelOptions = lazySchema(() =>
|
|
|
189
189
|
})
|
|
190
190
|
.optional(),
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Optional. When set to true, function call arguments will be streamed
|
|
194
|
+
* incrementally via partialArgs in streaming responses. Only supported
|
|
195
|
+
* on the Vertex AI API (not the Gemini API).
|
|
196
|
+
*
|
|
197
|
+
* @default true
|
|
198
|
+
*
|
|
199
|
+
* https://docs.cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling#streaming-fc
|
|
200
|
+
*/
|
|
201
|
+
streamFunctionCallArguments: z.boolean().optional(),
|
|
202
|
+
|
|
192
203
|
/**
|
|
193
204
|
* Optional. The service tier to use for the request.
|
|
194
205
|
*/
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
export type PartialArg = {
|
|
2
|
+
jsonPath: string;
|
|
3
|
+
stringValue?: string | null;
|
|
4
|
+
numberValue?: number | null;
|
|
5
|
+
boolValue?: boolean | null;
|
|
6
|
+
nullValue?: unknown;
|
|
7
|
+
willContinue?: boolean | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type PathSegment = string | number;
|
|
11
|
+
|
|
12
|
+
type StackEntry = {
|
|
13
|
+
segment: PathSegment;
|
|
14
|
+
isArray: boolean;
|
|
15
|
+
childCount: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Incrementally builds a JSON object from Google's streaming `partialArgs`
|
|
20
|
+
* chunks emitted during tool-call function calling. Tracks both the structured
|
|
21
|
+
* object and a running JSON text representation so callers can emit text deltas
|
|
22
|
+
* that, when concatenated, form valid nested JSON matching JSON.stringify output.
|
|
23
|
+
*
|
|
24
|
+
* Input: [{jsonPath:"$.location",stringValue:"Boston"}]
|
|
25
|
+
* Output: '{"location":"Boston"', then finalize() → closingDelta='}'
|
|
26
|
+
*/
|
|
27
|
+
export class GoogleJSONAccumulator {
|
|
28
|
+
private accumulatedArgs: Record<string, unknown> = {};
|
|
29
|
+
private jsonText = '';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stack representing the currently "open" containers in the JSON output.
|
|
33
|
+
* Entry 0 is always the root `{` object once the first value is written.
|
|
34
|
+
*/
|
|
35
|
+
private pathStack: StackEntry[] = [];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a string value is currently "open" (willContinue was true),
|
|
39
|
+
* meaning the closing quote has not yet been emitted.
|
|
40
|
+
*/
|
|
41
|
+
private stringOpen = false;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Input: [{jsonPath:"$.brightness",numberValue:50}]
|
|
45
|
+
* Output: { currentJSON:{brightness:50}, textDelta:'{"brightness":50' }
|
|
46
|
+
*/
|
|
47
|
+
processPartialArgs(partialArgs: PartialArg[]): {
|
|
48
|
+
currentJSON: Record<string, unknown>;
|
|
49
|
+
textDelta: string;
|
|
50
|
+
} {
|
|
51
|
+
let delta = '';
|
|
52
|
+
|
|
53
|
+
for (const arg of partialArgs) {
|
|
54
|
+
const rawPath = arg.jsonPath.replace(/^\$\./, '');
|
|
55
|
+
if (!rawPath) continue;
|
|
56
|
+
|
|
57
|
+
const segments = parsePath(rawPath);
|
|
58
|
+
|
|
59
|
+
const existingValue = getNestedValue(this.accumulatedArgs, segments);
|
|
60
|
+
const isStringContinuation =
|
|
61
|
+
arg.stringValue != null && existingValue !== undefined;
|
|
62
|
+
|
|
63
|
+
if (isStringContinuation) {
|
|
64
|
+
const escaped = JSON.stringify(arg.stringValue).slice(1, -1);
|
|
65
|
+
setNestedValue(
|
|
66
|
+
this.accumulatedArgs,
|
|
67
|
+
segments,
|
|
68
|
+
(existingValue as string) + arg.stringValue,
|
|
69
|
+
);
|
|
70
|
+
delta += escaped;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resolved = resolvePartialArgValue(arg);
|
|
75
|
+
if (resolved == null) continue;
|
|
76
|
+
|
|
77
|
+
setNestedValue(this.accumulatedArgs, segments, resolved.value);
|
|
78
|
+
delta += this.emitNavigationTo(segments, arg, resolved.json);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.jsonText += delta;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
currentJSON: this.accumulatedArgs,
|
|
85
|
+
textDelta: delta,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Input: jsonText='{"brightness":50', accumulatedArgs={brightness:50}
|
|
91
|
+
* Output: { finalJSON:'{"brightness":50}', closingDelta:'}' }
|
|
92
|
+
*/
|
|
93
|
+
finalize(): { finalJSON: string; closingDelta: string } {
|
|
94
|
+
const finalArgs = JSON.stringify(this.accumulatedArgs);
|
|
95
|
+
const closingDelta = finalArgs.slice(this.jsonText.length);
|
|
96
|
+
return { finalJSON: finalArgs, closingDelta };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Input: pathStack=[] (first call) or pathStack=[root,...] (subsequent calls)
|
|
101
|
+
* Output: '{' (first call) or '' (subsequent calls)
|
|
102
|
+
*/
|
|
103
|
+
private ensureRoot(): string {
|
|
104
|
+
if (this.pathStack.length === 0) {
|
|
105
|
+
this.pathStack.push({ segment: '', isArray: false, childCount: 0 });
|
|
106
|
+
return '{';
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Emits the JSON text fragment needed to navigate from the current open
|
|
113
|
+
* path to the new leaf at `targetSegments`, then writes the value.
|
|
114
|
+
*
|
|
115
|
+
* Input: targetSegments=["recipe","name"], arg={jsonPath:"$.recipe.name",stringValue:"Lasagna"}, valueJson='"Lasagna"'
|
|
116
|
+
* Output: '{"recipe":{"name":"Lasagna"'
|
|
117
|
+
*/
|
|
118
|
+
private emitNavigationTo(
|
|
119
|
+
targetSegments: PathSegment[],
|
|
120
|
+
arg: PartialArg,
|
|
121
|
+
valueJson: string,
|
|
122
|
+
): string {
|
|
123
|
+
let fragment = '';
|
|
124
|
+
|
|
125
|
+
if (this.stringOpen) {
|
|
126
|
+
fragment += '"';
|
|
127
|
+
this.stringOpen = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fragment += this.ensureRoot();
|
|
131
|
+
|
|
132
|
+
const targetContainerSegments = targetSegments.slice(0, -1);
|
|
133
|
+
const leafSegment = targetSegments[targetSegments.length - 1];
|
|
134
|
+
|
|
135
|
+
const commonDepth = this.findCommonStackDepth(targetContainerSegments);
|
|
136
|
+
|
|
137
|
+
fragment += this.closeDownTo(commonDepth);
|
|
138
|
+
fragment += this.openDownTo(targetContainerSegments, leafSegment);
|
|
139
|
+
fragment += this.emitLeaf(leafSegment, arg, valueJson);
|
|
140
|
+
|
|
141
|
+
return fragment;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns the stack depth to preserve when navigating to a new target
|
|
146
|
+
* container path. Always >= 1 (the root is never popped).
|
|
147
|
+
*
|
|
148
|
+
* Input: stack=[root,"recipe","ingredients",0], target=["recipe","ingredients",1]
|
|
149
|
+
* Output: 3 (keep root+"recipe"+"ingredients")
|
|
150
|
+
*/
|
|
151
|
+
private findCommonStackDepth(targetContainer: PathSegment[]): number {
|
|
152
|
+
const maxDepth = Math.min(
|
|
153
|
+
this.pathStack.length - 1,
|
|
154
|
+
targetContainer.length,
|
|
155
|
+
);
|
|
156
|
+
let common = 0;
|
|
157
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
158
|
+
if (this.pathStack[i + 1].segment === targetContainer[i]) {
|
|
159
|
+
common++;
|
|
160
|
+
} else {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return common + 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Closes containers from the current stack depth back down to `targetDepth`.
|
|
169
|
+
*
|
|
170
|
+
* Input: this.pathStack=[root,"recipe","ingredients",0], targetDepth=3
|
|
171
|
+
* Output: '}'
|
|
172
|
+
*/
|
|
173
|
+
private closeDownTo(targetDepth: number): string {
|
|
174
|
+
let fragment = '';
|
|
175
|
+
while (this.pathStack.length > targetDepth) {
|
|
176
|
+
const entry = this.pathStack.pop()!;
|
|
177
|
+
fragment += entry.isArray ? ']' : '}';
|
|
178
|
+
}
|
|
179
|
+
return fragment;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Opens containers from the current stack depth down to the full target
|
|
184
|
+
* container path, emitting opening `{`, `[`, keys, and commas as needed.
|
|
185
|
+
* `leafSegment` is used to determine if the innermost container is an array.
|
|
186
|
+
*
|
|
187
|
+
* Input: this.pathStack=[root], targetContainer=["recipe","ingredients"], leafSegment=0
|
|
188
|
+
* Output: '"recipe":{"ingredients":['
|
|
189
|
+
*/
|
|
190
|
+
private openDownTo(
|
|
191
|
+
targetContainer: PathSegment[],
|
|
192
|
+
leafSegment: PathSegment,
|
|
193
|
+
): string {
|
|
194
|
+
let fragment = '';
|
|
195
|
+
|
|
196
|
+
const startIdx = this.pathStack.length - 1;
|
|
197
|
+
|
|
198
|
+
for (let i = startIdx; i < targetContainer.length; i++) {
|
|
199
|
+
const seg = targetContainer[i];
|
|
200
|
+
const parentEntry = this.pathStack[this.pathStack.length - 1];
|
|
201
|
+
|
|
202
|
+
if (parentEntry.childCount > 0) {
|
|
203
|
+
fragment += ',';
|
|
204
|
+
}
|
|
205
|
+
parentEntry.childCount++;
|
|
206
|
+
|
|
207
|
+
if (typeof seg === 'string') {
|
|
208
|
+
fragment += `${JSON.stringify(seg)}:`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const childSeg =
|
|
212
|
+
i + 1 < targetContainer.length ? targetContainer[i + 1] : leafSegment;
|
|
213
|
+
const isArray = typeof childSeg === 'number';
|
|
214
|
+
|
|
215
|
+
fragment += isArray ? '[' : '{';
|
|
216
|
+
|
|
217
|
+
this.pathStack.push({ segment: seg, isArray, childCount: 0 });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return fragment;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Emits the comma, key, and value for a leaf entry in the current container.
|
|
225
|
+
*
|
|
226
|
+
* Input: leafSegment="name", arg={stringValue:"Lasagna"}, valueJson='"Lasagna"'
|
|
227
|
+
* Output: '"name":"Lasagna"' (or ',"name":"Lasagna"' if container.childCount > 0)
|
|
228
|
+
*/
|
|
229
|
+
private emitLeaf(
|
|
230
|
+
leafSegment: PathSegment,
|
|
231
|
+
arg: PartialArg,
|
|
232
|
+
valueJson: string,
|
|
233
|
+
): string {
|
|
234
|
+
let fragment = '';
|
|
235
|
+
const container = this.pathStack[this.pathStack.length - 1];
|
|
236
|
+
|
|
237
|
+
if (container.childCount > 0) {
|
|
238
|
+
fragment += ',';
|
|
239
|
+
}
|
|
240
|
+
container.childCount++;
|
|
241
|
+
|
|
242
|
+
if (typeof leafSegment === 'string') {
|
|
243
|
+
fragment += `${JSON.stringify(leafSegment)}:`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (arg.stringValue != null && arg.willContinue) {
|
|
247
|
+
fragment += valueJson.slice(0, -1);
|
|
248
|
+
this.stringOpen = true;
|
|
249
|
+
} else {
|
|
250
|
+
fragment += valueJson;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return fragment;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Splits a dotted/bracketed JSON path like `recipe.ingredients[0].name` into segments.
|
|
259
|
+
*
|
|
260
|
+
* Input: "recipe.ingredients[0].name"
|
|
261
|
+
* Output: ["recipe", "ingredients", 0, "name"]
|
|
262
|
+
*/
|
|
263
|
+
function parsePath(rawPath: string): Array<string | number> {
|
|
264
|
+
const segments: Array<string | number> = [];
|
|
265
|
+
for (const part of rawPath.split('.')) {
|
|
266
|
+
const bracketIdx = part.indexOf('[');
|
|
267
|
+
if (bracketIdx === -1) {
|
|
268
|
+
segments.push(part);
|
|
269
|
+
} else {
|
|
270
|
+
if (bracketIdx > 0) segments.push(part.slice(0, bracketIdx));
|
|
271
|
+
for (const m of part.matchAll(/\[(\d+)\]/g)) {
|
|
272
|
+
segments.push(parseInt(m[1], 10));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return segments;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Traverses a nested object along the given path segments and returns the leaf value.
|
|
281
|
+
*
|
|
282
|
+
* Input: ({recipe:{name:"Lasagna"}}, ["recipe","name"])
|
|
283
|
+
* Output: "Lasagna"
|
|
284
|
+
*/
|
|
285
|
+
function getNestedValue(
|
|
286
|
+
obj: Record<string, unknown>,
|
|
287
|
+
segments: Array<string | number>,
|
|
288
|
+
): unknown {
|
|
289
|
+
let current: unknown = obj;
|
|
290
|
+
for (const seg of segments) {
|
|
291
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
292
|
+
current = (current as Record<string | number, unknown>)[seg];
|
|
293
|
+
}
|
|
294
|
+
return current;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sets a value at a nested path, creating intermediate objects or arrays as needed.
|
|
299
|
+
*
|
|
300
|
+
* Input: obj={}, segments=["recipe","ingredients",0,"name"], value="Noodles"
|
|
301
|
+
* Output: {recipe:{ingredients:[{name:"Noodles"}]}}
|
|
302
|
+
*/
|
|
303
|
+
function setNestedValue(
|
|
304
|
+
obj: Record<string, unknown>,
|
|
305
|
+
segments: Array<string | number>,
|
|
306
|
+
value: unknown,
|
|
307
|
+
): void {
|
|
308
|
+
let current: Record<string | number, unknown> = obj;
|
|
309
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
310
|
+
const seg = segments[i];
|
|
311
|
+
const nextSeg = segments[i + 1];
|
|
312
|
+
if (current[seg] == null) {
|
|
313
|
+
current[seg] = typeof nextSeg === 'number' ? [] : {};
|
|
314
|
+
}
|
|
315
|
+
current = current[seg] as Record<string | number, unknown>;
|
|
316
|
+
}
|
|
317
|
+
current[segments[segments.length - 1]] = value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Extracts the first non-null typed value from a partial arg and returns it with its JSON representation.
|
|
322
|
+
*
|
|
323
|
+
* Input: arg={stringValue:"Boston"} or arg={numberValue:50}
|
|
324
|
+
* Output: {value:"Boston", json:'"Boston"'} or {value:50, json:'50'}
|
|
325
|
+
*/
|
|
326
|
+
function resolvePartialArgValue(arg: {
|
|
327
|
+
stringValue?: string | null;
|
|
328
|
+
numberValue?: number | null;
|
|
329
|
+
boolValue?: boolean | null;
|
|
330
|
+
nullValue?: unknown;
|
|
331
|
+
}): { value: unknown; json: string } | undefined {
|
|
332
|
+
const value = arg.stringValue ?? arg.numberValue ?? arg.boolValue;
|
|
333
|
+
if (value != null) return { value, json: JSON.stringify(value) };
|
|
334
|
+
if ('nullValue' in arg) return { value: null, json: 'null' };
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|