@voxgig/sdkgen 4.2.6 → 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/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/py/ReadmeExamplesTest_py.ts +15 -5
- 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/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,419 @@
|
|
|
1
|
+
// ProjectName SDK — cost feature (mirrors ts src/feature/cost/CostFeature.ts).
|
|
2
|
+
// Cost tracking and spend budget. Uses BOTH seams, which is the point of the
|
|
3
|
+
// feature: money is spent per HTTP ATTEMPT (a retried call is charged again,
|
|
4
|
+
// because the upstream API charges it again), but it is owed by an OPERATION.
|
|
5
|
+
// So the transport wrap prices each attempt, and preDone attributes the
|
|
6
|
+
// running total to `<entity>.<op>` and to the caller (the per-call ctrl
|
|
7
|
+
// actor, the same actor the audit feature records).
|
|
8
|
+
//
|
|
9
|
+
// The price of an attempt comes from the first source that answers: a
|
|
10
|
+
// response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
11
|
+
// `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
|
|
12
|
+
// (`path` x `perUnit`, e.g. "usage.total_tokens") is read at preDone instead,
|
|
13
|
+
// from the already-parsed result, and describes the whole call, so it
|
|
14
|
+
// REPLACES the per-attempt estimate rather than adding to it.
|
|
15
|
+
//
|
|
16
|
+
// `budget` caps total spend. With `onBudget: "deny"` a further operation is
|
|
17
|
+
// refused at prePoint, before an endpoint is resolved and before anything
|
|
18
|
+
// reaches the network.
|
|
19
|
+
//
|
|
20
|
+
// ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
21
|
+
// cache is charged for money that was never spent. The default (map) order
|
|
22
|
+
// puts cache innermost and cost outside it, so activate them in list form
|
|
23
|
+
// with cost first.
|
|
24
|
+
|
|
25
|
+
#ifndef SDK_FEATURE_COST_HPP
|
|
26
|
+
#define SDK_FEATURE_COST_HPP
|
|
27
|
+
|
|
28
|
+
#include <exception>
|
|
29
|
+
#include <functional>
|
|
30
|
+
#include <map>
|
|
31
|
+
#include <string>
|
|
32
|
+
#include <vector>
|
|
33
|
+
|
|
34
|
+
#include "../core/types.hpp"
|
|
35
|
+
#include "base.hpp"
|
|
36
|
+
#include "options.hpp"
|
|
37
|
+
|
|
38
|
+
namespace sdk {
|
|
39
|
+
|
|
40
|
+
class CostFeature : public BaseFeature {
|
|
41
|
+
public:
|
|
42
|
+
struct CostBucket {
|
|
43
|
+
int calls = 0;
|
|
44
|
+
double amount = 0.0;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
struct CostTotal {
|
|
48
|
+
int calls = 0;
|
|
49
|
+
int attempts = 0;
|
|
50
|
+
double amount = 0.0;
|
|
51
|
+
double reported = 0.0;
|
|
52
|
+
double estimated = 0.0;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
struct CostBudget {
|
|
56
|
+
double limit = 0.0;
|
|
57
|
+
double spent = 0.0;
|
|
58
|
+
double remaining = 0.0;
|
|
59
|
+
bool exceeded = false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
SdkClient* client = nullptr;
|
|
63
|
+
Value options = Value::undef();
|
|
64
|
+
|
|
65
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
66
|
+
std::string currency = "USD";
|
|
67
|
+
CostTotal total;
|
|
68
|
+
std::map<std::string, CostBucket> ops;
|
|
69
|
+
std::map<std::string, CostBucket> actors;
|
|
70
|
+
CostBudget budget;
|
|
71
|
+
Value last = Value::undef();
|
|
72
|
+
|
|
73
|
+
CostFeature() : BaseFeature("cost", "0.0.1", true) {}
|
|
74
|
+
|
|
75
|
+
void init(CtxPtr ctx, const Value& options_) override {
|
|
76
|
+
client = ctx->client;
|
|
77
|
+
options = options_;
|
|
78
|
+
active = fopt::foptBool(options, "active", false);
|
|
79
|
+
|
|
80
|
+
double limit = fopt::foptNum(options, "budget", 0.0);
|
|
81
|
+
|
|
82
|
+
currency = fopt::foptStr(options, "currency", "USD");
|
|
83
|
+
total = CostTotal();
|
|
84
|
+
ops.clear();
|
|
85
|
+
actors.clear();
|
|
86
|
+
budget = CostBudget();
|
|
87
|
+
budget.limit = limit;
|
|
88
|
+
budget.remaining = limit;
|
|
89
|
+
last = Value::undef();
|
|
90
|
+
pending.clear();
|
|
91
|
+
seq = 0;
|
|
92
|
+
|
|
93
|
+
if (!active) return;
|
|
94
|
+
|
|
95
|
+
auto inner = ctx->utility->fetcher;
|
|
96
|
+
ctx->utility->fetcher = [this, inner](CtxPtr ctx2, const std::string& url,
|
|
97
|
+
const Value& fetchdef) -> Value {
|
|
98
|
+
return charge(ctx2, url, fetchdef, inner);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The budget gate. Runs before endpoint resolution, so a refused call costs
|
|
103
|
+
// nothing at all.
|
|
104
|
+
void prePoint(CtxPtr ctx) override {
|
|
105
|
+
if (!active) return;
|
|
106
|
+
|
|
107
|
+
// Mark the context as running through the pipeline, so charge knows a
|
|
108
|
+
// preDone is coming and does not commit the spend itself.
|
|
109
|
+
pending[ctx->id].piped = true;
|
|
110
|
+
|
|
111
|
+
if (budget.limit <= 0.0 || total.amount < budget.limit) return;
|
|
112
|
+
|
|
113
|
+
budget.exceeded = true;
|
|
114
|
+
|
|
115
|
+
if (fopt::foptStr(options, "onBudget", "warn") != "deny") return;
|
|
116
|
+
|
|
117
|
+
auto err = ctx->makeError("cost_budget",
|
|
118
|
+
"Cost budget of " + numstr(budget.limit) + " " + currency +
|
|
119
|
+
" is spent (" + numstr(total.amount) + " " + currency +
|
|
120
|
+
" used)");
|
|
121
|
+
// Short-circuit endpoint resolution; makePoint surfaces this error.
|
|
122
|
+
ctx->out.pointError = err;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Attribute the operation's spend once the call is finished.
|
|
126
|
+
void preDone(CtxPtr ctx) override {
|
|
127
|
+
finish(ctx, true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A failed operation still spent the money. When the pipeline throws,
|
|
131
|
+
// preDone never runs, so without this the attempts are counted and the spend
|
|
132
|
+
// is not, and a budget could never see the cost of a failed call. Whichever
|
|
133
|
+
// hook fires first consumes the pending entry, so it commits exactly once.
|
|
134
|
+
void preUnexpected(CtxPtr ctx) override {
|
|
135
|
+
finish(ctx, false);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private:
|
|
139
|
+
// Per-operation accumulator, keyed by ctx id (the same shape metrics uses
|
|
140
|
+
// for its start markers).
|
|
141
|
+
struct Pending {
|
|
142
|
+
int attempts = 0;
|
|
143
|
+
double amount = 0.0;
|
|
144
|
+
double reported = 0.0;
|
|
145
|
+
double estimated = 0.0;
|
|
146
|
+
std::string source = "none";
|
|
147
|
+
// Set by prePoint. Its absence means the call never entered the pipeline
|
|
148
|
+
// (direct/graphql), so charge commits the spend itself.
|
|
149
|
+
bool piped = false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
std::map<std::string, Pending> pending;
|
|
153
|
+
int seq = 0;
|
|
154
|
+
|
|
155
|
+
void finish(CtxPtr ctx, bool done) {
|
|
156
|
+
if (!active) return;
|
|
157
|
+
|
|
158
|
+
auto it = pending.find(ctx->id);
|
|
159
|
+
if (it == pending.end()) return;
|
|
160
|
+
Pending p = it->second;
|
|
161
|
+
pending.erase(it);
|
|
162
|
+
|
|
163
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
164
|
+
// prePoint creates the pending entry to mark the context as piped, and
|
|
165
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
166
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
167
|
+
// call that never happened and file a zero-amount record as `last`.
|
|
168
|
+
//
|
|
169
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
170
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
171
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
172
|
+
if (!done && 0 == p.attempts) return;
|
|
173
|
+
|
|
174
|
+
std::string entity = "_";
|
|
175
|
+
std::string opname = "_";
|
|
176
|
+
if (ctx->op) {
|
|
177
|
+
if (!ctx->op->entity.empty()) entity = ctx->op->entity;
|
|
178
|
+
if (!ctx->op->name.empty()) opname = ctx->op->name;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
commit(ctx, p, entity, opname);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
185
|
+
// attribution, and the record. Shared by finish and the raw-request path in
|
|
186
|
+
// charge, which has no preDone to reach.
|
|
187
|
+
void commit(CtxPtr ctx, const Pending& p, const std::string& entity,
|
|
188
|
+
const std::string& opname) {
|
|
189
|
+
double amount = p.amount;
|
|
190
|
+
double reported = p.reported;
|
|
191
|
+
double estimated = p.estimated;
|
|
192
|
+
std::string source = p.source;
|
|
193
|
+
|
|
194
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
195
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
196
|
+
// amount counts as reported.
|
|
197
|
+
double body = 0.0;
|
|
198
|
+
if (bodyAmount(ctx, body)) {
|
|
199
|
+
amount = body;
|
|
200
|
+
reported = body;
|
|
201
|
+
estimated = 0.0;
|
|
202
|
+
source = "body";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
spend(amount, reported, estimated);
|
|
206
|
+
|
|
207
|
+
std::string actor = "anonymous";
|
|
208
|
+
std::string optActor = fopt::foptStr(options, "actor", "");
|
|
209
|
+
if (!optActor.empty()) actor = optActor;
|
|
210
|
+
if (ctx->ctrl && !ctx->ctrl->actor.empty()) actor = ctx->ctrl->actor;
|
|
211
|
+
|
|
212
|
+
total.calls++;
|
|
213
|
+
bump(ops, entity + "." + opname, amount);
|
|
214
|
+
bump(actors, actor, amount);
|
|
215
|
+
|
|
216
|
+
seq++;
|
|
217
|
+
Value record = vmap();
|
|
218
|
+
map_put(record, "seq", Value(seq));
|
|
219
|
+
map_put(record, "entity", Value(entity));
|
|
220
|
+
map_put(record, "op", Value(opname));
|
|
221
|
+
map_put(record, "actor", Value(actor));
|
|
222
|
+
map_put(record, "amount", Value(amount));
|
|
223
|
+
map_put(record, "currency", Value(currency));
|
|
224
|
+
map_put(record, "source", Value(source));
|
|
225
|
+
map_put(record, "attempts", Value(p.attempts));
|
|
226
|
+
last = record;
|
|
227
|
+
|
|
228
|
+
Value sink = getp(options, "sink");
|
|
229
|
+
if (sink.is_injector()) {
|
|
230
|
+
vs::Injection inj(Value::undef(), Value::undef());
|
|
231
|
+
sink.as_injector()(inj, vlist({record}), std::string(""), Value::undef());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
Value charge(CtxPtr ctx, const std::string& url, const Value& fetchdef,
|
|
236
|
+
const std::function<Value(CtxPtr, const std::string&, const Value&)>& inner) {
|
|
237
|
+
// A throwing transport still costs an attempt. Without this, a run of
|
|
238
|
+
// connection-level failures under `retry` (which catches and tries again)
|
|
239
|
+
// would be charged nothing at all, and an onBudget "deny" ceiling could
|
|
240
|
+
// never stop it.
|
|
241
|
+
Value res = Value::undef();
|
|
242
|
+
bool threw = false;
|
|
243
|
+
std::exception_ptr caught;
|
|
244
|
+
try {
|
|
245
|
+
res = inner(ctx, url, fetchdef);
|
|
246
|
+
} catch (...) {
|
|
247
|
+
threw = true;
|
|
248
|
+
caught = std::current_exception();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
std::string source = "none";
|
|
252
|
+
double amount = price(ctx, res, source);
|
|
253
|
+
|
|
254
|
+
// Accumulated here, committed once at preDone. Adding each attempt to the
|
|
255
|
+
// running total and then subtracting it again when a body figure
|
|
256
|
+
// supersedes it loses precision to catastrophic cancellation.
|
|
257
|
+
//
|
|
258
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from the
|
|
259
|
+
// rate table followed by a 200 carrying the cost header is part estimate,
|
|
260
|
+
// part reported, and collapsing both into the final attempt's category
|
|
261
|
+
// would corrupt the split.
|
|
262
|
+
Pending& p = pending[ctx->id];
|
|
263
|
+
p.attempts++;
|
|
264
|
+
p.amount += amount;
|
|
265
|
+
if ("header" == source || "body" == source) p.reported += amount;
|
|
266
|
+
else p.estimated += amount;
|
|
267
|
+
p.source = source;
|
|
268
|
+
|
|
269
|
+
total.attempts++;
|
|
270
|
+
|
|
271
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
272
|
+
// pipeline hooks, so there is no prePoint to gate on and no preDone to
|
|
273
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
274
|
+
if (!p.piped) {
|
|
275
|
+
Pending copy = p;
|
|
276
|
+
pending.erase(ctx->id);
|
|
277
|
+
commit(ctx, copy, "_", "direct");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (threw) {
|
|
281
|
+
std::rethrow_exception(caught);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return res;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Price one attempt: a reported header figure, else the rate table, else
|
|
288
|
+
// the flat unit.
|
|
289
|
+
double price(CtxPtr ctx, const Value& res, std::string& source) {
|
|
290
|
+
std::string header = fopt::foptStr(options, "header", "");
|
|
291
|
+
if (!header.empty()) {
|
|
292
|
+
std::string raw = fopt::fresHeader(res, header);
|
|
293
|
+
if (!raw.empty()) {
|
|
294
|
+
try {
|
|
295
|
+
double n = std::stod(raw);
|
|
296
|
+
source = "header";
|
|
297
|
+
return n * perUnit();
|
|
298
|
+
} catch (...) {
|
|
299
|
+
// Not a number: fall through to the rate table.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
double rate = 0.0;
|
|
305
|
+
if (rateFor(ctx, rate)) {
|
|
306
|
+
source = "table";
|
|
307
|
+
return rate;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
double unit = fopt::foptNum(options, "unit", 0.0);
|
|
311
|
+
if (unit != 0.0) {
|
|
312
|
+
source = "unit";
|
|
313
|
+
return unit;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
source = "none";
|
|
317
|
+
return 0.0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
321
|
+
// `<entity>.<op>`, then `<op>`, then `*`.
|
|
322
|
+
bool rateFor(CtxPtr ctx, double& out) {
|
|
323
|
+
Value rates = fopt::foptMap(options, "rates");
|
|
324
|
+
if (is_nullish(rates)) return false;
|
|
325
|
+
|
|
326
|
+
std::string entity = "";
|
|
327
|
+
if (ctx->entity != nullptr) entity = ctx->entity->getName();
|
|
328
|
+
else if (ctx->op) entity = ctx->op->entity;
|
|
329
|
+
std::string opname = ctx->op ? ctx->op->name : "";
|
|
330
|
+
|
|
331
|
+
for (const std::string& key : {entity + "." + opname, opname, std::string("*")}) {
|
|
332
|
+
Value v = getp(rates, key);
|
|
333
|
+
if (v.is_number()) {
|
|
334
|
+
out = v.as_double();
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// A usage figure from the parsed result body, priced by perUnit. Read here,
|
|
342
|
+
// not at the transport seam, because the body is one-shot.
|
|
343
|
+
bool bodyAmount(CtxPtr ctx, double& out) {
|
|
344
|
+
std::string path = fopt::foptStr(options, "path", "");
|
|
345
|
+
if (path.empty() || !ctx->result) return false;
|
|
346
|
+
|
|
347
|
+
Value body = ctx->result->body;
|
|
348
|
+
if (is_nullish(body)) return false;
|
|
349
|
+
|
|
350
|
+
Value v = Struct::getpath(body, splitDots(path));
|
|
351
|
+
if (v.is_number()) {
|
|
352
|
+
out = v.as_double() * perUnit();
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
if (v.is_string()) {
|
|
356
|
+
try {
|
|
357
|
+
out = std::stod(v.as_string()) * perUnit();
|
|
358
|
+
return true;
|
|
359
|
+
} catch (...) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
void spend(double amount, double reported, double estimated) {
|
|
367
|
+
total.amount += amount;
|
|
368
|
+
total.reported += reported;
|
|
369
|
+
total.estimated += estimated;
|
|
370
|
+
|
|
371
|
+
budget.spent = total.amount;
|
|
372
|
+
if (budget.limit > 0.0) {
|
|
373
|
+
budget.remaining = budget.limit - total.amount;
|
|
374
|
+
if (budget.remaining < 0.0) budget.remaining = 0.0;
|
|
375
|
+
if (total.amount >= budget.limit) budget.exceeded = true;
|
|
376
|
+
} else {
|
|
377
|
+
budget.remaining = 0.0;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
void bump(std::map<std::string, CostBucket>& bucket, const std::string& key,
|
|
382
|
+
double amount) {
|
|
383
|
+
CostBucket& b = bucket[key];
|
|
384
|
+
b.calls++;
|
|
385
|
+
b.amount += amount;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
double perUnit() { return fopt::foptNum(options, "perUnit", 0.0); }
|
|
389
|
+
|
|
390
|
+
// Render a money amount without an exponent or trailing zeros.
|
|
391
|
+
static std::string numstr(double n) {
|
|
392
|
+
std::string s = std::to_string(n);
|
|
393
|
+
std::size_t last = s.find_last_not_of('0');
|
|
394
|
+
if (last != std::string::npos && s[last] == '.') last--;
|
|
395
|
+
return s.substr(0, last + 1);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The body figure is a dot path; Struct::getpath takes segments. (paging
|
|
399
|
+
// carries its own copy for the same reason: feature source is trimmed per
|
|
400
|
+
// project, so a shared helper would vanish with whichever feature owns it.)
|
|
401
|
+
static std::vector<std::string> splitDots(const std::string& path) {
|
|
402
|
+
std::vector<std::string> out;
|
|
403
|
+
std::string cur;
|
|
404
|
+
for (char c : path) {
|
|
405
|
+
if (c == '.') {
|
|
406
|
+
if (!cur.empty()) out.push_back(cur);
|
|
407
|
+
cur.clear();
|
|
408
|
+
} else {
|
|
409
|
+
cur.push_back(c);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (!cur.empty()) out.push_back(cur);
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
} // namespace sdk
|
|
418
|
+
|
|
419
|
+
#endif // SDK_FEATURE_COST_HPP
|
|
File without changes
|