@voxgig/sdkgen 4.2.6 → 4.2.8
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/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/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,424 @@
|
|
|
1
|
+
package feature
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strconv"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
vs "github.com/voxgig/struct"
|
|
8
|
+
|
|
9
|
+
"GOMODULE/core"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Cost tracking and spend budget. Uses BOTH seams, which is the point of
|
|
13
|
+
// the feature: money is spent per HTTP ATTEMPT (a retried call is charged
|
|
14
|
+
// again, because the upstream API charges it again), but it is owed by an
|
|
15
|
+
// OPERATION. So the transport wrap prices each attempt, and PreDone
|
|
16
|
+
// attributes the running total to `<entity>.<op>` and to the caller
|
|
17
|
+
// (`ctrl.actor`, the same actor the audit feature records).
|
|
18
|
+
//
|
|
19
|
+
// The price of an attempt comes from the first source that answers: a
|
|
20
|
+
// response header (`header` x `perUnit`), the rate table (`rates`, keyed
|
|
21
|
+
// `<entity>.<op>` / `<op>` / `*`), then the flat `unit`. A body figure
|
|
22
|
+
// (`path` x `perUnit`, e.g. "usage.total_tokens") is read at PreDone
|
|
23
|
+
// instead, from the already-parsed result, and describes the whole call, so
|
|
24
|
+
// it REPLACES the per-attempt estimate rather than adding to it.
|
|
25
|
+
//
|
|
26
|
+
// `budget` caps total spend. With `onBudget: "deny"` a further operation is
|
|
27
|
+
// refused at PrePoint (via ctx.Out["point"], which MakePoint surfaces),
|
|
28
|
+
// before an endpoint is resolved and before anything reaches the network.
|
|
29
|
+
//
|
|
30
|
+
// ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
|
|
31
|
+
// cache is charged for money that was never spent. The default (map) order
|
|
32
|
+
// puts cache innermost and cost outside it, so activate them in array form
|
|
33
|
+
// with cost first.
|
|
34
|
+
type CostFeature struct {
|
|
35
|
+
BaseFeature
|
|
36
|
+
client *core.ProjectNameSDK
|
|
37
|
+
options map[string]any
|
|
38
|
+
|
|
39
|
+
// Aggregates (mirrors the ts client._cost record).
|
|
40
|
+
Currency string
|
|
41
|
+
Total *CostTotal
|
|
42
|
+
Ops map[string]*CostBucket
|
|
43
|
+
Actors map[string]*CostBucket
|
|
44
|
+
Budget *CostBudget
|
|
45
|
+
Last map[string]any
|
|
46
|
+
|
|
47
|
+
seq int
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type CostTotal struct {
|
|
51
|
+
Calls int
|
|
52
|
+
Attempts int
|
|
53
|
+
Amount float64
|
|
54
|
+
Reported float64
|
|
55
|
+
Estimated float64
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type CostBucket struct {
|
|
59
|
+
Calls int
|
|
60
|
+
Amount float64
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type CostBudget struct {
|
|
64
|
+
Limit float64
|
|
65
|
+
Spent float64
|
|
66
|
+
Remaining float64
|
|
67
|
+
Exceeded bool
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Per-operation accumulator, carried on ctx.Out between the transport wrap
|
|
71
|
+
// and PreDone.
|
|
72
|
+
type costPending struct {
|
|
73
|
+
attempts int
|
|
74
|
+
amount float64
|
|
75
|
+
reported float64
|
|
76
|
+
estimated float64
|
|
77
|
+
source string
|
|
78
|
+
// Set by PrePoint. Its absence means the call never entered the pipeline
|
|
79
|
+
// (direct/graphql), so charge commits the spend itself.
|
|
80
|
+
piped bool
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const costPendingKey = "cost_pending"
|
|
84
|
+
|
|
85
|
+
func NewCostFeature() *CostFeature {
|
|
86
|
+
return &CostFeature{
|
|
87
|
+
BaseFeature: BaseFeature{
|
|
88
|
+
Version: "0.0.1",
|
|
89
|
+
Name: "cost",
|
|
90
|
+
Active: true,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func (f *CostFeature) Init(ctx *core.Context, options map[string]any) {
|
|
96
|
+
f.client = ctx.Client
|
|
97
|
+
f.options = options
|
|
98
|
+
f.Active = foptBool(options, "active", false)
|
|
99
|
+
|
|
100
|
+
limit := foptNum(options, "budget", 0)
|
|
101
|
+
|
|
102
|
+
f.Currency = foptStr(options, "currency", "USD")
|
|
103
|
+
f.Total = &CostTotal{}
|
|
104
|
+
f.Ops = map[string]*CostBucket{}
|
|
105
|
+
f.Actors = map[string]*CostBucket{}
|
|
106
|
+
f.Budget = &CostBudget{Limit: limit, Remaining: limit}
|
|
107
|
+
f.seq = 0
|
|
108
|
+
|
|
109
|
+
if !f.Active {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
inner := ctx.Utility.Fetcher
|
|
114
|
+
|
|
115
|
+
ctx.Utility.Fetcher = func(ctx2 *core.Context, url string, fetchdef map[string]any) (any, error) {
|
|
116
|
+
return f.charge(ctx2, url, fetchdef, inner)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// PrePoint is the budget gate. It runs before endpoint resolution, so a
|
|
121
|
+
// refused call costs nothing at all.
|
|
122
|
+
func (f *CostFeature) PrePoint(ctx *core.Context) {
|
|
123
|
+
if !f.Active {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Mark the context as running through the pipeline, so charge knows a
|
|
128
|
+
// PreDone is coming and does not commit the spend itself.
|
|
129
|
+
pending, ok := ctx.Out[costPendingKey].(*costPending)
|
|
130
|
+
if !ok || pending == nil {
|
|
131
|
+
pending = &costPending{source: "none"}
|
|
132
|
+
ctx.Out[costPendingKey] = pending
|
|
133
|
+
}
|
|
134
|
+
pending.piped = true
|
|
135
|
+
|
|
136
|
+
limit := f.Budget.Limit
|
|
137
|
+
if limit <= 0 {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if f.Total.Amount < limit {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
f.Budget.Exceeded = true
|
|
145
|
+
|
|
146
|
+
if foptStr(f.options, "onBudget", "warn") != "deny" {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
err := ctx.MakeError("cost_budget",
|
|
151
|
+
"Cost budget of "+fcostNumStr(limit)+" "+f.Currency+" is spent ("+
|
|
152
|
+
fcostNumStr(f.Total.Amount)+" "+f.Currency+" used)")
|
|
153
|
+
|
|
154
|
+
// Short-circuit endpoint resolution; MakePoint surfaces this error
|
|
155
|
+
// before any network activity.
|
|
156
|
+
ctx.Out["point"] = err
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func (f *CostFeature) charge(ctx *core.Context, url string, fetchdef map[string]any,
|
|
160
|
+
inner core.FetcherFunc) (any, error) {
|
|
161
|
+
|
|
162
|
+
res, err := inner(ctx, url, fetchdef)
|
|
163
|
+
|
|
164
|
+
amount, source := f.price(ctx, res)
|
|
165
|
+
|
|
166
|
+
pending, ok := ctx.Out[costPendingKey].(*costPending)
|
|
167
|
+
if !ok || pending == nil {
|
|
168
|
+
pending = &costPending{source: "none"}
|
|
169
|
+
ctx.Out[costPendingKey] = pending
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pending.attempts++
|
|
173
|
+
|
|
174
|
+
// Accumulated here, committed once at PreDone. Adding each attempt to
|
|
175
|
+
// the running total and then subtracting it again when a body figure
|
|
176
|
+
// supersedes it loses precision to catastrophic cancellation.
|
|
177
|
+
//
|
|
178
|
+
// Reported and estimated are kept apart per ATTEMPT: a 503 priced from
|
|
179
|
+
// the rate table followed by a 200 carrying the cost header is part
|
|
180
|
+
// estimate, part reported, and collapsing both into the final attempt's
|
|
181
|
+
// category would corrupt the split.
|
|
182
|
+
//
|
|
183
|
+
// A failed transport is an error VALUE here, not a panic, so it already
|
|
184
|
+
// reaches this point and is priced from the table or unit — no separate
|
|
185
|
+
// rescue is needed, unlike the ts/js ports.
|
|
186
|
+
pending.amount += amount
|
|
187
|
+
if source == "header" || source == "body" {
|
|
188
|
+
pending.reported += amount
|
|
189
|
+
} else {
|
|
190
|
+
pending.estimated += amount
|
|
191
|
+
}
|
|
192
|
+
pending.source = source
|
|
193
|
+
|
|
194
|
+
f.Total.Attempts++
|
|
195
|
+
|
|
196
|
+
// direct() and graphql() reach the transport without dispatching any
|
|
197
|
+
// pipeline hooks, so there is no PrePoint to gate on and no PreDone to
|
|
198
|
+
// commit. Their spend is committed here, or it would never be counted.
|
|
199
|
+
if !pending.piped {
|
|
200
|
+
f.commit(ctx, pending, "_", "direct")
|
|
201
|
+
delete(ctx.Out, costPendingKey)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return res, err
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// PreDone attributes the operation's spend once the call is finished.
|
|
208
|
+
func (f *CostFeature) PreDone(ctx *core.Context) {
|
|
209
|
+
f.finish(ctx, true)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// PreUnexpected commits a FAILED operation's spend. When the pipeline errors,
|
|
213
|
+
// PreDone never runs, so without this the attempts are counted and the spend
|
|
214
|
+
// is not, and a budget could never see the cost of a failed call. Whichever
|
|
215
|
+
// hook fires first consumes the pending entry, so it commits exactly once.
|
|
216
|
+
func (f *CostFeature) PreUnexpected(ctx *core.Context) {
|
|
217
|
+
f.finish(ctx, false)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func (f *CostFeature) finish(ctx *core.Context, done bool) {
|
|
221
|
+
if !f.Active {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
pending, ok := ctx.Out[costPendingKey].(*costPending)
|
|
225
|
+
if !ok || pending == nil {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
delete(ctx.Out, costPendingKey)
|
|
229
|
+
|
|
230
|
+
// A FAILED operation that made no attempt never reached the network:
|
|
231
|
+
// PrePoint creates the pending entry to mark the context as piped, and
|
|
232
|
+
// then the budget gate refuses the call (rbac, or an unresolvable
|
|
233
|
+
// endpoint, short-circuits just as early). Committing it would count a
|
|
234
|
+
// call that never happened and file a zero-amount record as Last.
|
|
235
|
+
//
|
|
236
|
+
// A SUCCEEDED operation that made no attempt is the opposite case: it was
|
|
237
|
+
// served from the cache. That is a real call, and the fact that it cost
|
|
238
|
+
// nothing is the whole point of ordering cost inside the cache.
|
|
239
|
+
if !done && pending.attempts == 0 {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
entity := "_"
|
|
244
|
+
opname := "_"
|
|
245
|
+
if ctx.Op != nil {
|
|
246
|
+
entity = ctx.Op.Entity
|
|
247
|
+
opname = ctx.Op.Name
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
f.commit(ctx, pending, entity, opname)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// commit records one operation's spend: totals, budget, per-op and per-actor
|
|
254
|
+
// attribution, and the record. Shared by finish and the raw-request path in
|
|
255
|
+
// charge, which has no PreDone to reach.
|
|
256
|
+
func (f *CostFeature) commit(ctx *core.Context, pending *costPending, entity, opname string) {
|
|
257
|
+
amount := pending.amount
|
|
258
|
+
reported := pending.reported
|
|
259
|
+
estimated := pending.estimated
|
|
260
|
+
source := pending.source
|
|
261
|
+
|
|
262
|
+
// A body figure prices the whole call, so it replaces the per-attempt
|
|
263
|
+
// estimate rather than adding to it, and being server-stated the whole
|
|
264
|
+
// amount counts as reported.
|
|
265
|
+
if body, has := f.body(ctx); has {
|
|
266
|
+
amount = body
|
|
267
|
+
reported = body
|
|
268
|
+
estimated = 0
|
|
269
|
+
source = "body"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
f.spend(amount, reported, estimated)
|
|
273
|
+
|
|
274
|
+
actor := "anonymous"
|
|
275
|
+
if a := foptStr(f.options, "actor", ""); a != "" {
|
|
276
|
+
actor = a
|
|
277
|
+
}
|
|
278
|
+
if ctx.Ctrl != nil && ctx.Ctrl.Actor != "" {
|
|
279
|
+
actor = ctx.Ctrl.Actor
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
f.Total.Calls++
|
|
283
|
+
f.bump(f.Ops, entity+"."+opname, amount)
|
|
284
|
+
f.bump(f.Actors, actor, amount)
|
|
285
|
+
|
|
286
|
+
f.seq++
|
|
287
|
+
record := map[string]any{
|
|
288
|
+
"seq": f.seq,
|
|
289
|
+
"entity": entity,
|
|
290
|
+
"op": opname,
|
|
291
|
+
"actor": actor,
|
|
292
|
+
"amount": amount,
|
|
293
|
+
"currency": f.Currency,
|
|
294
|
+
"source": source,
|
|
295
|
+
"attempts": pending.attempts,
|
|
296
|
+
}
|
|
297
|
+
f.Last = record
|
|
298
|
+
|
|
299
|
+
if sink, ok := f.options["sink"].(func(map[string]any)); ok {
|
|
300
|
+
sink(record)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// price returns the cost of one attempt: a reported header figure, else the
|
|
305
|
+
// rate table, else the flat unit.
|
|
306
|
+
func (f *CostFeature) price(ctx *core.Context, res any) (float64, string) {
|
|
307
|
+
if header := foptStr(f.options, "header", ""); header != "" {
|
|
308
|
+
if s, ok := fresHeader(res, header); ok {
|
|
309
|
+
if n, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
|
|
310
|
+
return n * f.perUnit(), "header"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if rate, ok := f.rate(ctx); ok {
|
|
316
|
+
return rate, "table"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if unit := foptNum(f.options, "unit", 0); unit != 0 {
|
|
320
|
+
return unit, "unit"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return 0, "none"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// rate uses the same lookup grammar as rbac's rules: `<entity>.<op>`, then
|
|
327
|
+
// `<op>`, then `*`.
|
|
328
|
+
func (f *CostFeature) rate(ctx *core.Context) (float64, bool) {
|
|
329
|
+
rates := foptMap(f.options, "rates")
|
|
330
|
+
if rates == nil {
|
|
331
|
+
return 0, false
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
entity := ""
|
|
335
|
+
if ctx.Entity != nil {
|
|
336
|
+
entity = ctx.Entity.GetName()
|
|
337
|
+
} else if ctx.Op != nil {
|
|
338
|
+
entity = ctx.Op.Entity
|
|
339
|
+
}
|
|
340
|
+
opname := ""
|
|
341
|
+
if ctx.Op != nil {
|
|
342
|
+
opname = ctx.Op.Name
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for _, key := range []string{entity + "." + opname, opname, "*"} {
|
|
346
|
+
if v, has := rates[key]; has {
|
|
347
|
+
switch n := v.(type) {
|
|
348
|
+
case int:
|
|
349
|
+
return float64(n), true
|
|
350
|
+
case int64:
|
|
351
|
+
return float64(n), true
|
|
352
|
+
case float64:
|
|
353
|
+
return n, true
|
|
354
|
+
case float32:
|
|
355
|
+
return float64(n), true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return 0, false
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// body reads a usage figure from the parsed result body, priced by perUnit.
|
|
363
|
+
// Read here, not at the transport seam, because the body is one-shot.
|
|
364
|
+
func (f *CostFeature) body(ctx *core.Context) (float64, bool) {
|
|
365
|
+
path := foptStr(f.options, "path", "")
|
|
366
|
+
if path == "" || ctx.Result == nil || ctx.Result.Body == nil {
|
|
367
|
+
return 0, false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
switch n := vs.GetPath(path, ctx.Result.Body).(type) {
|
|
371
|
+
case int:
|
|
372
|
+
return float64(n) * f.perUnit(), true
|
|
373
|
+
case int64:
|
|
374
|
+
return float64(n) * f.perUnit(), true
|
|
375
|
+
case float64:
|
|
376
|
+
return n * f.perUnit(), true
|
|
377
|
+
case float32:
|
|
378
|
+
return float64(n) * f.perUnit(), true
|
|
379
|
+
case string:
|
|
380
|
+
if v, err := strconv.ParseFloat(strings.TrimSpace(n), 64); err == nil {
|
|
381
|
+
return v * f.perUnit(), true
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return 0, false
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
func (f *CostFeature) spend(amount, reported, estimated float64) {
|
|
388
|
+
f.Total.Amount += amount
|
|
389
|
+
f.Total.Reported += reported
|
|
390
|
+
f.Total.Estimated += estimated
|
|
391
|
+
|
|
392
|
+
limit := f.Budget.Limit
|
|
393
|
+
f.Budget.Spent = f.Total.Amount
|
|
394
|
+
if limit > 0 {
|
|
395
|
+
f.Budget.Remaining = limit - f.Total.Amount
|
|
396
|
+
if f.Budget.Remaining < 0 {
|
|
397
|
+
f.Budget.Remaining = 0
|
|
398
|
+
}
|
|
399
|
+
if f.Total.Amount >= limit {
|
|
400
|
+
f.Budget.Exceeded = true
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
f.Budget.Remaining = 0
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
func (f *CostFeature) bump(bucket map[string]*CostBucket, key string, amount float64) {
|
|
408
|
+
b := bucket[key]
|
|
409
|
+
if b == nil {
|
|
410
|
+
b = &CostBucket{}
|
|
411
|
+
bucket[key] = b
|
|
412
|
+
}
|
|
413
|
+
b.Calls++
|
|
414
|
+
b.Amount += amount
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
func (f *CostFeature) perUnit() float64 {
|
|
418
|
+
return foptNum(f.options, "perUnit", 0)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// fcostNumStr renders a money amount without an exponent or trailing zeros.
|
|
422
|
+
func fcostNumStr(n float64) string {
|
|
423
|
+
return strconv.FormatFloat(n, 'f', -1, 64)
|
|
424
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|