@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,439 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
declare(strict_types=1);
|
|
3
|
+
|
|
4
|
+
// ProjectName SDK cost feature
|
|
5
|
+
|
|
6
|
+
require_once __DIR__ . '/BaseFeature.php';
|
|
7
|
+
|
|
8
|
+
// Cost tracking and spend budget. Uses BOTH seams, which is the point of
|
|
9
|
+
// the feature: money is spent per HTTP ATTEMPT (a retried call is charged
|
|
10
|
+
// again, because the upstream API charges it again), but it is owed by an
|
|
11
|
+
// OPERATION. So the transport wrap prices each attempt, and PreDone
|
|
12
|
+
// attributes the running total to `<entity>.<op>` and to the caller
|
|
13
|
+
// (`ctrl->actor`, the same actor the audit feature records).
|
|
14
|
+
//
|
|
15
|
+
// The price of an attempt comes from the first source that answers: a
|
|
16
|
+
// response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
17
|
+
// `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
|
|
18
|
+
// (`path` x `perUnit`, e.g. 'usage.total_tokens') is read at PreDone
|
|
19
|
+
// instead, from the already-parsed result, and describes the whole call, so
|
|
20
|
+
// it REPLACES the per-attempt estimate rather than adding to it.
|
|
21
|
+
//
|
|
22
|
+
// `budget` caps total spend. With `onBudget: 'deny'` a further operation is
|
|
23
|
+
// refused at PrePoint (the error is placed in `$ctx->out['point']`), before
|
|
24
|
+
// an endpoint is resolved and before anything reaches the network.
|
|
25
|
+
//
|
|
26
|
+
// ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
27
|
+
// cache is charged for money that was never spent. The default (map) order
|
|
28
|
+
// puts cache innermost and cost outside it, so activate them in list form
|
|
29
|
+
// with cost first. Mirrors ts/src/feature/cost/CostFeature.ts.
|
|
30
|
+
class ProjectNameCostFeature extends ProjectNameBaseFeature
|
|
31
|
+
{
|
|
32
|
+
private mixed $client;
|
|
33
|
+
private ?array $options;
|
|
34
|
+
private ?\WeakMap $pending;
|
|
35
|
+
private int $seq;
|
|
36
|
+
|
|
37
|
+
public function __construct()
|
|
38
|
+
{
|
|
39
|
+
parent::__construct();
|
|
40
|
+
$this->version = '0.0.1';
|
|
41
|
+
$this->name = 'cost';
|
|
42
|
+
$this->active = true;
|
|
43
|
+
$this->client = null;
|
|
44
|
+
$this->options = null;
|
|
45
|
+
$this->pending = null;
|
|
46
|
+
$this->seq = 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function init(ProjectNameContext $ctx, array $options): void
|
|
50
|
+
{
|
|
51
|
+
$this->client = $ctx->client;
|
|
52
|
+
$this->options = $options;
|
|
53
|
+
$this->active = ($options['active'] ?? null) === true;
|
|
54
|
+
$this->pending = new \WeakMap();
|
|
55
|
+
$this->seq = 0;
|
|
56
|
+
|
|
57
|
+
$limit = $this->_limit();
|
|
58
|
+
|
|
59
|
+
$client = $this->client;
|
|
60
|
+
if (!isset($client->_cost)) {
|
|
61
|
+
$client->_cost = [
|
|
62
|
+
'currency' => (string)($this->options['currency'] ?? 'USD'),
|
|
63
|
+
'total' => [
|
|
64
|
+
'calls' => 0, 'attempts' => 0,
|
|
65
|
+
'amount' => 0.0, 'reported' => 0.0, 'estimated' => 0.0,
|
|
66
|
+
],
|
|
67
|
+
'ops' => [],
|
|
68
|
+
'actors' => [],
|
|
69
|
+
'budget' => [
|
|
70
|
+
'limit' => $limit, 'spent' => 0.0,
|
|
71
|
+
'remaining' => $limit, 'exceeded' => false,
|
|
72
|
+
],
|
|
73
|
+
'last' => null,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!$this->active) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$utility = $ctx->utility;
|
|
82
|
+
$inner = $utility->fetcher;
|
|
83
|
+
|
|
84
|
+
$utility->fetcher = function (ProjectNameContext $ctx2, string $url, array $fetchdef) use ($inner): array {
|
|
85
|
+
return $this->_charge($ctx2, $url, $fetchdef, $inner);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The budget gate. Runs before endpoint resolution, so a refused call
|
|
90
|
+
// costs nothing at all.
|
|
91
|
+
public function PrePoint(ProjectNameContext $ctx): void
|
|
92
|
+
{
|
|
93
|
+
if (!$this->active) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Mark the context as running through the pipeline, so _charge knows a
|
|
98
|
+
// PreDone is coming and does not commit the spend itself.
|
|
99
|
+
if ($this->pending !== null) {
|
|
100
|
+
$entry = isset($this->pending[$ctx]) ? $this->pending[$ctx] : $this->_newPending();
|
|
101
|
+
$entry['piped'] = true;
|
|
102
|
+
$this->pending[$ctx] = $entry;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$limit = $this->_limit();
|
|
106
|
+
if ($limit <= 0.0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
$client = $this->client;
|
|
111
|
+
if ($client->_cost['total']['amount'] < $limit) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$cost = $client->_cost;
|
|
116
|
+
$cost['budget']['exceeded'] = true;
|
|
117
|
+
$client->_cost = $cost;
|
|
118
|
+
|
|
119
|
+
if (($this->options['onBudget'] ?? 'warn') !== 'deny') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
$err = $ctx->make_error('cost_budget',
|
|
124
|
+
'Cost budget of ' . $this->_numstr($limit) . ' ' . $cost['currency'] .
|
|
125
|
+
' is spent (' . $this->_numstr((float)$cost['total']['amount']) . ' ' .
|
|
126
|
+
$cost['currency'] . ' used)');
|
|
127
|
+
|
|
128
|
+
// Short-circuit endpoint resolution; the pipeline surfaces this error.
|
|
129
|
+
$ctx->out['point'] = $err;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private function _charge(ProjectNameContext $ctx, string $url, array $fetchdef, callable $inner): array
|
|
133
|
+
{
|
|
134
|
+
// A rejecting transport still costs an attempt. Without this, a run of
|
|
135
|
+
// connection-level failures under `retry` (which catches and tries
|
|
136
|
+
// again) would be charged nothing at all, and an onBudget 'deny'
|
|
137
|
+
// ceiling could never stop it.
|
|
138
|
+
$threw = null;
|
|
139
|
+
try {
|
|
140
|
+
[$res, $err] = $inner($ctx, $url, $fetchdef);
|
|
141
|
+
} catch (\Throwable $ex) {
|
|
142
|
+
$threw = $ex;
|
|
143
|
+
$res = null;
|
|
144
|
+
$err = $ex;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[$amount, $source] = $this->_price($ctx, $res);
|
|
148
|
+
|
|
149
|
+
$entry = ($this->pending !== null && isset($this->pending[$ctx]))
|
|
150
|
+
? $this->pending[$ctx]
|
|
151
|
+
: $this->_newPending();
|
|
152
|
+
|
|
153
|
+
$entry['attempts']++;
|
|
154
|
+
|
|
155
|
+
// Accumulated here, committed once at PreDone. Adding each attempt to
|
|
156
|
+
// the running total and then subtracting it again when a body figure
|
|
157
|
+
// supersedes it loses precision to catastrophic cancellation.
|
|
158
|
+
//
|
|
159
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from
|
|
160
|
+
// the rate table followed by a 200 carrying the cost header is part
|
|
161
|
+
// estimate, part reported, and collapsing both into the final
|
|
162
|
+
// attempt's category would corrupt the split.
|
|
163
|
+
$entry['amount'] += $amount;
|
|
164
|
+
$entry[('header' === $source || 'body' === $source) ? 'reported' : 'estimated'] += $amount;
|
|
165
|
+
$entry['source'] = $source;
|
|
166
|
+
|
|
167
|
+
if ($this->pending !== null) {
|
|
168
|
+
$this->pending[$ctx] = $entry;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
$client = $this->client;
|
|
172
|
+
$cost = $client->_cost;
|
|
173
|
+
$cost['total']['attempts']++;
|
|
174
|
+
$client->_cost = $cost;
|
|
175
|
+
|
|
176
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
177
|
+
// pipeline hooks, so there is no PrePoint to gate on and no PreDone to
|
|
178
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
179
|
+
// `piped` is set by PrePoint, so its absence is the signal.
|
|
180
|
+
if (!$entry['piped']) {
|
|
181
|
+
$this->_commit($ctx, $entry, '_', 'direct');
|
|
182
|
+
if ($this->pending !== null) {
|
|
183
|
+
unset($this->pending[$ctx]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if ($threw !== null) {
|
|
188
|
+
throw $threw;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return [$res, $err];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private function _newPending(): array
|
|
195
|
+
{
|
|
196
|
+
return [
|
|
197
|
+
'attempts' => 0, 'amount' => 0.0,
|
|
198
|
+
'reported' => 0.0, 'estimated' => 0.0,
|
|
199
|
+
'source' => 'none', 'piped' => false,
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Attribute the operation's spend once the call is finished.
|
|
204
|
+
public function PreDone(ProjectNameContext $ctx): void
|
|
205
|
+
{
|
|
206
|
+
$this->_finish($ctx, true);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A failed operation still spent the money. When the pipeline throws,
|
|
210
|
+
// PreDone never runs, so without this the attempts are counted and the
|
|
211
|
+
// spend is not, and a budget could never see the cost of a failed call.
|
|
212
|
+
// Whichever hook fires first consumes the pending entry, so it commits
|
|
213
|
+
// exactly once.
|
|
214
|
+
public function PreUnexpected(ProjectNameContext $ctx): void
|
|
215
|
+
{
|
|
216
|
+
$this->_finish($ctx, false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private function _finish(ProjectNameContext $ctx, bool $done): void
|
|
220
|
+
{
|
|
221
|
+
if (!$this->active || $this->pending === null || !isset($this->pending[$ctx])) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
$entry = $this->pending[$ctx];
|
|
225
|
+
unset($this->pending[$ctx]);
|
|
226
|
+
|
|
227
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
228
|
+
// PrePoint creates the pending entry to mark the context as piped, and
|
|
229
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
230
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
231
|
+
// call that never happened and file a zero-amount record as `last`.
|
|
232
|
+
//
|
|
233
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
234
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
235
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
236
|
+
if (!$done && 0 === $entry['attempts']) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
$entity = ($ctx->op !== null && $ctx->op->entity !== '') ? $ctx->op->entity : '_';
|
|
241
|
+
$opname = ($ctx->op !== null && $ctx->op->name !== '') ? $ctx->op->name : '_';
|
|
242
|
+
|
|
243
|
+
$this->_commit($ctx, $entry, $entity, $opname);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Commit one operation's spend: totals, budget, per-op and per-actor
|
|
247
|
+
// attribution, and the record. Shared by _finish and the raw-request path
|
|
248
|
+
// in _charge, which has no PreDone to reach.
|
|
249
|
+
private function _commit(ProjectNameContext $ctx, array $entry, string $entity, string $opname): void
|
|
250
|
+
{
|
|
251
|
+
$client = $this->client;
|
|
252
|
+
$cost = $client->_cost;
|
|
253
|
+
|
|
254
|
+
$amount = (float)$entry['amount'];
|
|
255
|
+
$reported = (float)$entry['reported'];
|
|
256
|
+
$estimated = (float)$entry['estimated'];
|
|
257
|
+
$source = (string)$entry['source'];
|
|
258
|
+
|
|
259
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
260
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
261
|
+
// amount counts as reported.
|
|
262
|
+
$body = $this->_body($ctx);
|
|
263
|
+
if ($body !== null) {
|
|
264
|
+
$amount = $body;
|
|
265
|
+
$reported = $body;
|
|
266
|
+
$estimated = 0.0;
|
|
267
|
+
$source = 'body';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
$cost = $this->_spend($cost, $amount, $reported, $estimated);
|
|
271
|
+
|
|
272
|
+
// ctrl->actor is an optional extension property on the control
|
|
273
|
+
// object, same as the audit feature reads.
|
|
274
|
+
$ctrl_actor = $ctx->ctrl->actor ?? null;
|
|
275
|
+
$actor = (is_string($ctrl_actor) && $ctrl_actor !== '') ? $ctrl_actor
|
|
276
|
+
: (string)($this->options['actor'] ?? 'anonymous');
|
|
277
|
+
|
|
278
|
+
$cost['total']['calls']++;
|
|
279
|
+
$cost['ops'] = $this->_bump($cost['ops'], $entity . '.' . $opname, $amount);
|
|
280
|
+
$cost['actors'] = $this->_bump($cost['actors'], $actor, $amount);
|
|
281
|
+
|
|
282
|
+
$this->seq++;
|
|
283
|
+
$record = [
|
|
284
|
+
'seq' => $this->seq,
|
|
285
|
+
'entity' => $entity,
|
|
286
|
+
'op' => $opname,
|
|
287
|
+
'actor' => $actor,
|
|
288
|
+
'amount' => $amount,
|
|
289
|
+
'currency' => $cost['currency'],
|
|
290
|
+
'source' => $source,
|
|
291
|
+
'attempts' => $entry['attempts'],
|
|
292
|
+
];
|
|
293
|
+
$cost['last'] = $record;
|
|
294
|
+
$client->_cost = $cost;
|
|
295
|
+
|
|
296
|
+
$sink = $this->options['sink'] ?? null;
|
|
297
|
+
if (is_callable($sink)) {
|
|
298
|
+
try {
|
|
299
|
+
$sink($record);
|
|
300
|
+
} catch (\Throwable $e) {
|
|
301
|
+
// A failing sink must never take down the call.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Price one attempt: a reported header figure, else the rate table, else
|
|
307
|
+
// the flat unit.
|
|
308
|
+
private function _price(ProjectNameContext $ctx, mixed $res): array
|
|
309
|
+
{
|
|
310
|
+
$header = $this->options['header'] ?? '';
|
|
311
|
+
if (is_string($header) && '' !== $header) {
|
|
312
|
+
$val = $this->_header($res, $header);
|
|
313
|
+
if ($val !== null) {
|
|
314
|
+
return [$val * $this->_perUnit(), 'header'];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
$rate = $this->_rate($ctx);
|
|
319
|
+
if ($rate !== null) {
|
|
320
|
+
return [$rate, 'table'];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
$unit = $this->options['unit'] ?? 0;
|
|
324
|
+
if (is_numeric($unit) && 0.0 !== (float)$unit) {
|
|
325
|
+
return [(float)$unit, 'unit'];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return [0.0, 'none'];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The rate table uses the same lookup grammar as rbac's rules:
|
|
332
|
+
// `<entity>.<op>`, then `<op>`, then `*`.
|
|
333
|
+
private function _rate(ProjectNameContext $ctx): ?float
|
|
334
|
+
{
|
|
335
|
+
$rates = $this->options['rates'] ?? null;
|
|
336
|
+
if (!is_array($rates)) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
$entity = '';
|
|
341
|
+
if ($ctx->entity !== null && isset($ctx->entity->name)) {
|
|
342
|
+
$entity = (string)$ctx->entity->name;
|
|
343
|
+
} elseif ($ctx->op !== null) {
|
|
344
|
+
$entity = $ctx->op->entity;
|
|
345
|
+
}
|
|
346
|
+
$opname = $ctx->op !== null ? $ctx->op->name : '';
|
|
347
|
+
|
|
348
|
+
foreach ([$entity . '.' . $opname, $opname, '*'] as $key) {
|
|
349
|
+
$val = $rates[$key] ?? null;
|
|
350
|
+
if (is_numeric($val)) {
|
|
351
|
+
return (float)$val;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// A usage figure from the parsed result body, priced by perUnit. Read
|
|
358
|
+
// here, not at the transport seam, because the body is one-shot.
|
|
359
|
+
private function _body(ProjectNameContext $ctx): ?float
|
|
360
|
+
{
|
|
361
|
+
$path = $this->options['path'] ?? '';
|
|
362
|
+
if (!is_string($path) || '' === $path) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
if ($ctx->result === null || $ctx->result->body === null) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
$val = \Voxgig\Struct\Struct::getpath($ctx->result->body, $path);
|
|
369
|
+
if (!is_numeric($val)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return (float)$val * $this->_perUnit();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private function _spend(array $cost, float $amount, float $reported, float $estimated): array
|
|
376
|
+
{
|
|
377
|
+
$cost['total']['amount'] += $amount;
|
|
378
|
+
$cost['total']['reported'] += $reported;
|
|
379
|
+
$cost['total']['estimated'] += $estimated;
|
|
380
|
+
|
|
381
|
+
$limit = (float)$cost['budget']['limit'];
|
|
382
|
+
$cost['budget']['spent'] = $cost['total']['amount'];
|
|
383
|
+
if ($limit > 0.0) {
|
|
384
|
+
$cost['budget']['remaining'] = max(0.0, $limit - (float)$cost['total']['amount']);
|
|
385
|
+
if ((float)$cost['total']['amount'] >= $limit) {
|
|
386
|
+
$cost['budget']['exceeded'] = true;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
$cost['budget']['remaining'] = 0.0;
|
|
390
|
+
}
|
|
391
|
+
return $cost;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private function _bump(array $bucket, string $key, float $amount): array
|
|
395
|
+
{
|
|
396
|
+
if (!isset($bucket[$key])) {
|
|
397
|
+
$bucket[$key] = ['calls' => 0, 'amount' => 0.0];
|
|
398
|
+
}
|
|
399
|
+
$bucket[$key]['calls']++;
|
|
400
|
+
$bucket[$key]['amount'] += $amount;
|
|
401
|
+
return $bucket;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private function _header(mixed $res, string $name): ?float
|
|
405
|
+
{
|
|
406
|
+
if (!is_array($res)) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
$headers = $res['headers'] ?? null;
|
|
410
|
+
if (!is_array($headers)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
$lower = strtolower($name);
|
|
414
|
+
foreach ($headers as $key => $val) {
|
|
415
|
+
if (strtolower((string)$key) === $lower && is_numeric($val)) {
|
|
416
|
+
return (float)$val;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private function _perUnit(): float
|
|
423
|
+
{
|
|
424
|
+
$per = $this->options['perUnit'] ?? 0;
|
|
425
|
+
return is_numeric($per) ? (float)$per : 0.0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private function _limit(): float
|
|
429
|
+
{
|
|
430
|
+
$budget = $this->options['budget'] ?? 0;
|
|
431
|
+
return is_numeric($budget) ? (float)$budget : 0.0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Render a money amount without an exponent or trailing zeros.
|
|
435
|
+
private function _numstr(float $n): string
|
|
436
|
+
{
|
|
437
|
+
return rtrim(rtrim(number_format($n, 10, '.', ''), '0'), '.') ?: '0';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -179,7 +179,37 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
179
179
|
return $respond(200, $out);
|
|
180
180
|
|
|
181
181
|
} elseif ($op->name === 'list') {
|
|
182
|
-
|
|
182
|
+
// FILTER THE MATCH THE WAY THE TS MOCK DOES.
|
|
183
|
+
//
|
|
184
|
+
// This branch used the whole of reqmatch, while load goes
|
|
185
|
+
// through resolve_match and the TS mock runs every op through
|
|
186
|
+
// buildArgs — which keeps `id` and the point's REQUIRED params
|
|
187
|
+
// and drops the rest. buildArgs is right here in this file and
|
|
188
|
+
// its docblock says it mirrors the TS one; list simply did not
|
|
189
|
+
// call it.
|
|
190
|
+
//
|
|
191
|
+
// So a match key that is neither `id` nor a required route
|
|
192
|
+
// param matched NOTHING in php and was IGNORED in ts. trello's
|
|
193
|
+
// board_star lists on `board_id`, which is not a field of that
|
|
194
|
+
// entity (id, pos, member_id, id_board) and not a route param:
|
|
195
|
+
// php returned an empty list and BoardStarEntityTest failed on
|
|
196
|
+
// an assertion that ts, js and every other port passed.
|
|
197
|
+
//
|
|
198
|
+
// buildArgs returns a struct query; find_all takes a flat map,
|
|
199
|
+
// so take the primary key of each $OR clause. find_all already
|
|
200
|
+
// does its own alias fallback, which is what the second $OR
|
|
201
|
+
// element carries.
|
|
202
|
+
$listargs = $this->buildArgs($fctx, $op, $fctx->reqmatch);
|
|
203
|
+
$listmatch = [];
|
|
204
|
+
foreach (($listargs['$AND'] ?? []) as $clause) {
|
|
205
|
+
$ors = $clause['$OR'] ?? [];
|
|
206
|
+
if (is_array($ors) && count($ors) > 0 && is_array($ors[0])) {
|
|
207
|
+
foreach ($ors[0] as $lk => $lv) {
|
|
208
|
+
$listmatch[$lk] = $lv;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
$found = $find_all($entmap, $listmatch, $alias);
|
|
183
213
|
$cleaned = [];
|
|
184
214
|
foreach ($found as $e) {
|
|
185
215
|
if (is_array($e)) unset($e['$KEY']);
|
|
File without changes
|