@voxgig/sdkgen 4.2.5 → 4.2.7
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/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/naming.js +5 -4
- package/dist/helpers/naming.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/model/feature/cost.aon +43 -0
- package/project/.sdk/model/feature/feature-index.aon +1 -0
- package/project/.sdk/src/cmp/go/utility_go.ts +20 -7
- package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +2 -1
- package/project/.sdk/src/cmp/js/utility_js.ts +20 -7
- package/project/.sdk/src/cmp/php/ReadmeExamplesTest_php.ts +144 -12
- package/project/.sdk/src/cmp/py/ReadmeExamplesTest_py.ts +98 -3
- package/project/.sdk/src/cmp/rb/ReadmeExamplesTest_rb.ts +83 -10
- package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +2 -1
- package/project/.sdk/src/cmp/ts/utility_ts.ts +20 -7
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/feature/cost.c +549 -0
- package/project/.sdk/tm/c/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/clojure/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/cpp/feature/cost.hpp +419 -0
- package/project/.sdk/tm/cpp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/csharp/feature/CostFeature.cs +518 -0
- package/project/.sdk/tm/csharp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/dart/lib/feature/cost/CostFeature.dart +446 -0
- package/project/.sdk/tm/dart/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/elixir/lib/projectname/feature/cost.ex +442 -0
- package/project/.sdk/tm/elixir/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go/feature/cost_feature.go +424 -0
- package/project/.sdk/tm/go/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-cli/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/go-mcp/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/java/feature/CostFeature.java +431 -0
- package/project/.sdk/tm/java/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/js/src/feature/cost/CostFeature.js +413 -0
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +264 -0
- package/project/.sdk/tm/kotlin/feature/CostFeature.kt +430 -0
- package/project/.sdk/tm/kotlin/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lean/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/lua/feature/cost_feature.lua +457 -0
- package/project/.sdk/tm/lua/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ocaml/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/perl/feature/cost_feature.pm +446 -0
- package/project/.sdk/tm/perl/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/php/feature/CostFeature.php +439 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +31 -1
- package/project/.sdk/tm/php/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py/pkg/feature/cost_feature.py +381 -0
- package/project/.sdk/tm/py/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/py-data/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rb/feature/cost_feature.rb +386 -0
- package/project/.sdk/tm/rb/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/rust/feature/cost.rs +467 -0
- package/project/.sdk/tm/rust/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/scala/feature/CostFeature.scala +388 -0
- package/project/.sdk/tm/scala/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/CostFeature.swift +425 -0
- package/project/.sdk/tm/swift/src/feature/cost/.gitkeep +0 -0
- package/project/.sdk/tm/ts/src/feature/cost/CostFeature.ts +415 -0
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +266 -0
- package/project/.sdk/tm/zig/feature/cost.zig +457 -0
- package/project/.sdk/tm/zig/src/feature/cost/.gitkeep +0 -0
- package/project/sdkgen-package.json +2 -1
- package/src/helpers/naming.ts +5 -4
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
package JAVAPACKAGE.feature;
|
|
2
|
+
|
|
3
|
+
import java.util.Arrays;
|
|
4
|
+
import java.util.LinkedHashMap;
|
|
5
|
+
import java.util.List;
|
|
6
|
+
import java.util.Map;
|
|
7
|
+
import java.util.function.Consumer;
|
|
8
|
+
|
|
9
|
+
import JAVAPACKAGE.core.Context;
|
|
10
|
+
import JAVAPACKAGE.core.SdkClient;
|
|
11
|
+
import JAVAPACKAGE.core.Utility;
|
|
12
|
+
import JAVAPACKAGE.utility.struct.Struct;
|
|
13
|
+
|
|
14
|
+
// Cost tracking and spend budget. Uses BOTH seams, which is the point of
|
|
15
|
+
// the feature: money is spent per HTTP ATTEMPT (a retried call is charged
|
|
16
|
+
// again, because the upstream API charges it again), but it is owed by an
|
|
17
|
+
// OPERATION. So the transport wrap prices each attempt, and preDone
|
|
18
|
+
// attributes the running total to `<entity>.<op>` and to the caller (the
|
|
19
|
+
// per-call ctrl actor, the same actor the audit feature records).
|
|
20
|
+
//
|
|
21
|
+
// The price of an attempt comes from the first source that answers: a
|
|
22
|
+
// response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
23
|
+
// `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
|
|
24
|
+
// (`path` x `perUnit`, e.g. "usage.total_tokens") is read at preDone
|
|
25
|
+
// instead, from the already-parsed result, and describes the whole call, so
|
|
26
|
+
// it REPLACES the per-attempt estimate rather than adding to it.
|
|
27
|
+
//
|
|
28
|
+
// `budget` caps total spend. With `onBudget: "deny"` a further operation is
|
|
29
|
+
// refused at prePoint (via ctx.out["point"], which makePoint surfaces),
|
|
30
|
+
// before an endpoint is resolved and before anything reaches the network.
|
|
31
|
+
//
|
|
32
|
+
// ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
33
|
+
// cache is charged for money that was never spent. The default (map) order
|
|
34
|
+
// puts cache innermost and cost outside it, so activate them in list form
|
|
35
|
+
// with cost first.
|
|
36
|
+
public class CostFeature extends BaseFeature {
|
|
37
|
+
|
|
38
|
+
private SdkClient client;
|
|
39
|
+
private Map<String, Object> options;
|
|
40
|
+
private int seq = 0;
|
|
41
|
+
|
|
42
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
43
|
+
public String currency = "USD";
|
|
44
|
+
public CostTotal total = new CostTotal();
|
|
45
|
+
public Map<String, CostBucket> ops = new LinkedHashMap<>();
|
|
46
|
+
public Map<String, CostBucket> actors = new LinkedHashMap<>();
|
|
47
|
+
public CostBudget budget = new CostBudget();
|
|
48
|
+
public Map<String, Object> last = null;
|
|
49
|
+
|
|
50
|
+
public static class CostTotal {
|
|
51
|
+
public int calls = 0;
|
|
52
|
+
public int attempts = 0;
|
|
53
|
+
public double amount = 0.0;
|
|
54
|
+
public double reported = 0.0;
|
|
55
|
+
public double estimated = 0.0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static class CostBucket {
|
|
59
|
+
public int calls = 0;
|
|
60
|
+
public double amount = 0.0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public static class CostBudget {
|
|
64
|
+
public double limit = 0.0;
|
|
65
|
+
public double spent = 0.0;
|
|
66
|
+
public double remaining = 0.0;
|
|
67
|
+
public boolean exceeded = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Per-operation accumulator, carried on ctx.out between the transport wrap
|
|
71
|
+
// and preDone.
|
|
72
|
+
private static class CostPending {
|
|
73
|
+
int attempts = 0;
|
|
74
|
+
double amount = 0.0;
|
|
75
|
+
double reported = 0.0;
|
|
76
|
+
double estimated = 0.0;
|
|
77
|
+
String source = "none";
|
|
78
|
+
// Set by prePoint. Its absence means the call never entered the pipeline
|
|
79
|
+
// (direct/graphql), so charge commits the spend itself.
|
|
80
|
+
boolean piped = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static final String COST_PENDING_KEY = "cost_pending";
|
|
84
|
+
|
|
85
|
+
public CostFeature() {
|
|
86
|
+
super("cost", "0.0.1", true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Override
|
|
90
|
+
public void init(Context ctx, Map<String, Object> options) {
|
|
91
|
+
this.client = ctx.client;
|
|
92
|
+
this.options = options;
|
|
93
|
+
this.active = FeatureOptions.foptBool(options, "active", false);
|
|
94
|
+
this.seq = 0;
|
|
95
|
+
|
|
96
|
+
double limit = FeatureOptions.foptNum(options, "budget", 0.0);
|
|
97
|
+
|
|
98
|
+
this.currency = FeatureOptions.foptStr(options, "currency", "USD");
|
|
99
|
+
this.total = new CostTotal();
|
|
100
|
+
this.ops = new LinkedHashMap<>();
|
|
101
|
+
this.actors = new LinkedHashMap<>();
|
|
102
|
+
this.budget = new CostBudget();
|
|
103
|
+
this.budget.limit = limit;
|
|
104
|
+
this.budget.remaining = limit;
|
|
105
|
+
this.last = null;
|
|
106
|
+
|
|
107
|
+
if (!this.active) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
final Utility.FetcherFn inner = ctx.utility.fetcher;
|
|
112
|
+
|
|
113
|
+
ctx.utility.fetcher = (ctx2, url, fetchdef) -> charge(ctx2, url, fetchdef, inner);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The budget gate. Runs before endpoint resolution, so a refused call
|
|
117
|
+
// costs nothing at all.
|
|
118
|
+
@Override
|
|
119
|
+
public void prePoint(Context ctx) {
|
|
120
|
+
if (!this.active) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Mark the context as running through the pipeline, so charge knows a
|
|
125
|
+
// preDone is coming and does not commit the spend itself.
|
|
126
|
+
Object raw = ctx.out.get(COST_PENDING_KEY);
|
|
127
|
+
CostPending pending = (raw instanceof CostPending) ? (CostPending) raw : new CostPending();
|
|
128
|
+
pending.piped = true;
|
|
129
|
+
ctx.out.put(COST_PENDING_KEY, pending);
|
|
130
|
+
|
|
131
|
+
double limit = this.budget.limit;
|
|
132
|
+
if (limit <= 0.0 || this.total.amount < limit) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.budget.exceeded = true;
|
|
137
|
+
|
|
138
|
+
if (!"deny".equals(FeatureOptions.foptStr(this.options, "onBudget", "warn"))) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
RuntimeException err = ctx.makeError("cost_budget",
|
|
143
|
+
"Cost budget of " + numstr(limit) + " " + this.currency + " is spent ("
|
|
144
|
+
+ numstr(this.total.amount) + " " + this.currency + " used)");
|
|
145
|
+
|
|
146
|
+
// Short-circuit endpoint resolution; makePoint surfaces this error
|
|
147
|
+
// before any network activity.
|
|
148
|
+
ctx.out.put("point", err);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private Object charge(Context ctx, String url, Map<String, Object> fetchdef,
|
|
152
|
+
Utility.FetcherFn inner) {
|
|
153
|
+
|
|
154
|
+
// A throwing transport still costs an attempt. Without this, a run of
|
|
155
|
+
// connection-level failures under `retry` (which catches and tries
|
|
156
|
+
// again) would be charged nothing at all, and an onBudget "deny" ceiling
|
|
157
|
+
// could never stop it.
|
|
158
|
+
Object res = null;
|
|
159
|
+
RuntimeException threw = null;
|
|
160
|
+
try {
|
|
161
|
+
res = inner.fetch(ctx, url, fetchdef);
|
|
162
|
+
} catch (RuntimeException ex) {
|
|
163
|
+
threw = ex;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
double[] priced = new double[1];
|
|
167
|
+
String source = price(ctx, res, priced);
|
|
168
|
+
|
|
169
|
+
Object raw = ctx.out.get(COST_PENDING_KEY);
|
|
170
|
+
CostPending pending = (raw instanceof CostPending) ? (CostPending) raw : new CostPending();
|
|
171
|
+
ctx.out.put(COST_PENDING_KEY, pending);
|
|
172
|
+
|
|
173
|
+
pending.attempts++;
|
|
174
|
+
|
|
175
|
+
// Accumulated here, committed once at preDone. Adding each attempt to
|
|
176
|
+
// the running total and then subtracting it again when a body figure
|
|
177
|
+
// supersedes it loses precision to catastrophic cancellation.
|
|
178
|
+
//
|
|
179
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from
|
|
180
|
+
// the rate table followed by a 200 carrying the cost header is part
|
|
181
|
+
// estimate, part reported, and collapsing both into the final attempt's
|
|
182
|
+
// category would corrupt the split.
|
|
183
|
+
pending.amount += priced[0];
|
|
184
|
+
if ("header".equals(source) || "body".equals(source)) {
|
|
185
|
+
pending.reported += priced[0];
|
|
186
|
+
} else {
|
|
187
|
+
pending.estimated += priced[0];
|
|
188
|
+
}
|
|
189
|
+
pending.source = source;
|
|
190
|
+
|
|
191
|
+
this.total.attempts++;
|
|
192
|
+
|
|
193
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
194
|
+
// pipeline hooks, so there is no prePoint to gate on and no preDone to
|
|
195
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
196
|
+
if (!pending.piped) {
|
|
197
|
+
commit(ctx, pending, "_", "direct");
|
|
198
|
+
ctx.out.remove(COST_PENDING_KEY);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (threw != null) {
|
|
202
|
+
throw threw;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return res;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Attribute the operation's spend once the call is finished.
|
|
209
|
+
@Override
|
|
210
|
+
public void preDone(Context ctx) {
|
|
211
|
+
finish(ctx, true);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// A failed operation still spent the money. When the pipeline errors,
|
|
215
|
+
// preDone never runs, so without this the attempts are counted and the
|
|
216
|
+
// spend is not, and a budget could never see the cost of a failed call.
|
|
217
|
+
// Whichever hook fires first consumes the pending entry, so it commits
|
|
218
|
+
// exactly once.
|
|
219
|
+
@Override
|
|
220
|
+
public void preUnexpected(Context ctx) {
|
|
221
|
+
finish(ctx, false);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private void finish(Context ctx, boolean done) {
|
|
225
|
+
if (!this.active) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
Object raw = ctx.out.get(COST_PENDING_KEY);
|
|
229
|
+
if (!(raw instanceof CostPending)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
ctx.out.remove(COST_PENDING_KEY);
|
|
233
|
+
CostPending pending = (CostPending) raw;
|
|
234
|
+
|
|
235
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
236
|
+
// prePoint creates the pending entry to mark the context as piped, and
|
|
237
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
238
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
239
|
+
// call that never happened and file a zero-amount record as `last`.
|
|
240
|
+
//
|
|
241
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
242
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
243
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
244
|
+
if (!done && 0 == pending.attempts) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
String entity = (ctx.op != null && ctx.op.entity != null && !"".equals(ctx.op.entity))
|
|
249
|
+
? ctx.op.entity : "_";
|
|
250
|
+
String opname = (ctx.op != null && ctx.op.name != null && !"".equals(ctx.op.name))
|
|
251
|
+
? ctx.op.name : "_";
|
|
252
|
+
|
|
253
|
+
commit(ctx, pending, entity, opname);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
257
|
+
// attribution, and the record. Shared by finish and the raw-request path in
|
|
258
|
+
// charge, which has no preDone to reach.
|
|
259
|
+
@SuppressWarnings("unchecked")
|
|
260
|
+
private void commit(Context ctx, CostPending pending, String entity, String opname) {
|
|
261
|
+
double amount = pending.amount;
|
|
262
|
+
double reported = pending.reported;
|
|
263
|
+
double estimated = pending.estimated;
|
|
264
|
+
String source = pending.source;
|
|
265
|
+
|
|
266
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
267
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
268
|
+
// amount counts as reported.
|
|
269
|
+
Double body = bodyAmount(ctx);
|
|
270
|
+
if (body != null) {
|
|
271
|
+
amount = body;
|
|
272
|
+
reported = body;
|
|
273
|
+
estimated = 0.0;
|
|
274
|
+
source = "body";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
spend(amount, reported, estimated);
|
|
278
|
+
|
|
279
|
+
String actor = "anonymous";
|
|
280
|
+
String optActor = FeatureOptions.foptStr(this.options, "actor", "");
|
|
281
|
+
if (!"".equals(optActor)) {
|
|
282
|
+
actor = optActor;
|
|
283
|
+
}
|
|
284
|
+
if (ctx.ctrl != null && ctx.ctrl.actor != null && !"".equals(ctx.ctrl.actor)) {
|
|
285
|
+
actor = ctx.ctrl.actor;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.total.calls++;
|
|
289
|
+
bump(this.ops, entity + "." + opname, amount);
|
|
290
|
+
bump(this.actors, actor, amount);
|
|
291
|
+
|
|
292
|
+
this.seq++;
|
|
293
|
+
Map<String, Object> record = new LinkedHashMap<>();
|
|
294
|
+
record.put("seq", this.seq);
|
|
295
|
+
record.put("entity", entity);
|
|
296
|
+
record.put("op", opname);
|
|
297
|
+
record.put("actor", actor);
|
|
298
|
+
record.put("amount", amount);
|
|
299
|
+
record.put("currency", this.currency);
|
|
300
|
+
record.put("source", source);
|
|
301
|
+
record.put("attempts", pending.attempts);
|
|
302
|
+
this.last = record;
|
|
303
|
+
|
|
304
|
+
if (this.options.get("sink") instanceof Consumer) {
|
|
305
|
+
((Consumer<Map<String, Object>>) this.options.get("sink")).accept(record);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Price one attempt: a reported header figure, else the rate table, else
|
|
310
|
+
// the flat unit. The amount is returned through `out[0]`, the source by
|
|
311
|
+
// return value.
|
|
312
|
+
private String price(Context ctx, Object res, double[] out) {
|
|
313
|
+
String header = FeatureOptions.foptStr(this.options, "header", "");
|
|
314
|
+
if (!"".equals(header)) {
|
|
315
|
+
String raw = FeatureOptions.fresHeader(res, header);
|
|
316
|
+
Double n = num(raw);
|
|
317
|
+
if (n != null) {
|
|
318
|
+
out[0] = n * perUnit();
|
|
319
|
+
return "header";
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Double rate = rate(ctx);
|
|
324
|
+
if (rate != null) {
|
|
325
|
+
out[0] = rate;
|
|
326
|
+
return "table";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
double unit = FeatureOptions.foptNum(this.options, "unit", 0.0);
|
|
330
|
+
if (0.0 != unit) {
|
|
331
|
+
out[0] = unit;
|
|
332
|
+
return "unit";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
out[0] = 0.0;
|
|
336
|
+
return "none";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
340
|
+
// `<entity>.<op>`, then `<op>`, then `*`.
|
|
341
|
+
private Double rate(Context ctx) {
|
|
342
|
+
Map<String, Object> rates = FeatureOptions.foptMap(this.options, "rates");
|
|
343
|
+
if (rates == null || rates.isEmpty()) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
String entity = "";
|
|
348
|
+
if (ctx.entity != null) {
|
|
349
|
+
entity = ctx.entity.getName();
|
|
350
|
+
} else if (ctx.op != null && ctx.op.entity != null) {
|
|
351
|
+
entity = ctx.op.entity;
|
|
352
|
+
}
|
|
353
|
+
String opname = (ctx.op != null && ctx.op.name != null) ? ctx.op.name : "";
|
|
354
|
+
|
|
355
|
+
for (String key : new String[] { entity + "." + opname, opname, "*" }) {
|
|
356
|
+
Object val = rates.get(key);
|
|
357
|
+
if (val instanceof Number) {
|
|
358
|
+
return ((Number) val).doubleValue();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// A usage figure from the parsed result body, priced by perUnit. Read
|
|
365
|
+
// here, not at the transport seam, because the body is one-shot.
|
|
366
|
+
private Double bodyAmount(Context ctx) {
|
|
367
|
+
String path = FeatureOptions.foptStr(this.options, "path", "");
|
|
368
|
+
if ("".equals(path) || ctx.result == null || ctx.result.body == null) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
List<Object> segs = Arrays.asList((Object[]) path.split("\\."));
|
|
372
|
+
Object val = Struct.getpath(ctx.result.body, segs);
|
|
373
|
+
Double n = (val instanceof Number) ? ((Number) val).doubleValue() : num(val);
|
|
374
|
+
if (n == null) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return n * perUnit();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private void spend(double amount, double reported, double estimated) {
|
|
381
|
+
this.total.amount += amount;
|
|
382
|
+
this.total.reported += reported;
|
|
383
|
+
this.total.estimated += estimated;
|
|
384
|
+
|
|
385
|
+
double limit = this.budget.limit;
|
|
386
|
+
this.budget.spent = this.total.amount;
|
|
387
|
+
if (limit > 0.0) {
|
|
388
|
+
this.budget.remaining = Math.max(0.0, limit - this.total.amount);
|
|
389
|
+
if (this.total.amount >= limit) {
|
|
390
|
+
this.budget.exceeded = true;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
this.budget.remaining = 0.0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private void bump(Map<String, CostBucket> bucket, String key, double amount) {
|
|
398
|
+
CostBucket entry = bucket.get(key);
|
|
399
|
+
if (entry == null) {
|
|
400
|
+
entry = new CostBucket();
|
|
401
|
+
bucket.put(key, entry);
|
|
402
|
+
}
|
|
403
|
+
entry.calls++;
|
|
404
|
+
entry.amount += amount;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private Double num(Object val) {
|
|
408
|
+
if (val instanceof Number) {
|
|
409
|
+
return ((Number) val).doubleValue();
|
|
410
|
+
}
|
|
411
|
+
if (val instanceof String) {
|
|
412
|
+
try {
|
|
413
|
+
return Double.parseDouble(((String) val).trim());
|
|
414
|
+
} catch (NumberFormatException e) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private double perUnit() {
|
|
422
|
+
return FeatureOptions.foptNum(this.options, "perUnit", 0.0);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Render a money amount without an exponent or trailing zeros.
|
|
426
|
+
private String numstr(double n) {
|
|
427
|
+
String s = String.format("%.10f", n);
|
|
428
|
+
s = s.replaceAll("0+$", "").replaceAll("\\.$", "");
|
|
429
|
+
return "".equals(s) ? "0" : s;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
File without changes
|