@voxgig/sdkgen 4.4.1 → 4.5.0

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.
Files changed (97) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/FeatureDocs.d.ts +16 -0
  3. package/dist/cmp/FeatureDocs.js +72 -0
  4. package/dist/cmp/FeatureDocs.js.map +1 -0
  5. package/dist/cmp/Readme.js +2 -0
  6. package/dist/cmp/Readme.js.map +1 -1
  7. package/dist/cmp/ReadmeFeatures.d.ts +2 -0
  8. package/dist/cmp/ReadmeFeatures.js +95 -0
  9. package/dist/cmp/ReadmeFeatures.js.map +1 -0
  10. package/dist/cmp/ReadmeRef.js +4 -0
  11. package/dist/cmp/ReadmeRef.js.map +1 -1
  12. package/dist/cmp/ReadmeRefFeatures.d.ts +2 -0
  13. package/dist/cmp/ReadmeRefFeatures.js +130 -0
  14. package/dist/cmp/ReadmeRefFeatures.js.map +1 -0
  15. package/dist/cmp/ReadmeTop.js +46 -0
  16. package/dist/cmp/ReadmeTop.js.map +1 -1
  17. package/dist/helpers/opShape.js +5 -1
  18. package/dist/helpers/opShape.js.map +1 -1
  19. package/dist/sdkgen.d.ts +3 -2
  20. package/dist/sdkgen.js +30 -10
  21. package/dist/sdkgen.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/utility.d.ts +2 -1
  24. package/dist/utility.js +17 -1
  25. package/dist/utility.js.map +1 -1
  26. package/package.json +1 -1
  27. package/project/.sdk/src/cmp/c/ReadmeRef_c.ts +5 -0
  28. package/project/.sdk/src/cmp/clojure/ReadmeRef_clojure.ts +5 -0
  29. package/project/.sdk/src/cmp/cpp/ReadmeRef_cpp.ts +5 -0
  30. package/project/.sdk/src/cmp/csharp/ReadmeRef_csharp.ts +5 -0
  31. package/project/.sdk/src/cmp/dart/ReadmeRef_dart.ts +5 -0
  32. package/project/.sdk/src/cmp/elixir/ReadmeRef_elixir.ts +5 -0
  33. package/project/.sdk/src/cmp/go/ReadmeRef_go.ts +5 -0
  34. package/project/.sdk/src/cmp/go/TestDirect_go.ts +123 -2
  35. package/project/.sdk/src/cmp/java/ReadmeRef_java.ts +5 -0
  36. package/project/.sdk/src/cmp/js/ReadmeRef_js.ts +5 -0
  37. package/project/.sdk/src/cmp/js/TestDirect_js.ts +62 -0
  38. package/project/.sdk/src/cmp/kotlin/ReadmeRef_kotlin.ts +5 -0
  39. package/project/.sdk/src/cmp/lean/ReadmeRef_lean.ts +27 -3
  40. package/project/.sdk/src/cmp/lua/ReadmeRef_lua.ts +5 -0
  41. package/project/.sdk/src/cmp/ocaml/ReadmeRef_ocaml.ts +5 -0
  42. package/project/.sdk/src/cmp/perl/ReadmeRef_perl.ts +5 -0
  43. package/project/.sdk/src/cmp/php/ReadmeRef_php.ts +5 -0
  44. package/project/.sdk/src/cmp/py/ReadmeRef_py.ts +5 -0
  45. package/project/.sdk/src/cmp/rb/ReadmeRef_rb.ts +5 -0
  46. package/project/.sdk/src/cmp/rust/ReadmeRef_rust.ts +5 -0
  47. package/project/.sdk/src/cmp/scala/ReadmeRef_scala.ts +5 -0
  48. package/project/.sdk/src/cmp/seneca-provider/Extras_seneca-provider.ts +19 -1
  49. package/project/.sdk/src/cmp/seneca-provider/Main_seneca-provider.ts +27 -2
  50. package/project/.sdk/src/cmp/swift/ReadmeRef_swift.ts +5 -0
  51. package/project/.sdk/src/cmp/ts/Config_ts.ts +4 -1
  52. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +9 -4
  53. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +8 -3
  54. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +3 -2
  55. package/project/.sdk/src/cmp/ts/ReadmeRef_ts.ts +7 -2
  56. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +3 -2
  57. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +5 -2
  58. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +5 -2
  59. package/project/.sdk/src/cmp/zig/ReadmeRef_zig.ts +5 -0
  60. package/project/.sdk/tm/clojure/src/sdk/features.clj +175 -0
  61. package/project/.sdk/tm/dart/test/feature_test.dart +54 -0
  62. package/project/.sdk/tm/go/feature/test_feature.go +17 -6
  63. package/project/.sdk/tm/go/test/feature_corpus_test.go +10 -3
  64. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +5 -4
  65. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +8 -1
  66. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +3 -1
  67. package/project/.sdk/tm/js/test/feature/Corpus.test.js +13 -3
  68. package/project/.sdk/tm/js/test/feature.test.js +19 -9
  69. package/project/.sdk/tm/lean/src/SdkFeatures.lean +186 -1
  70. package/project/.sdk/tm/lua/feature/test_feature.lua +21 -7
  71. package/project/.sdk/tm/lua/test/feature_test.lua +18 -18
  72. package/project/.sdk/tm/lua/utility/make_options.lua +2 -0
  73. package/project/.sdk/tm/ocaml/sdk_features.ml +5 -0
  74. package/project/.sdk/tm/perl/t/feature_corpus.t +5 -4
  75. package/project/.sdk/tm/php/feature/TestFeature.php +19 -8
  76. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +7 -4
  77. package/project/.sdk/tm/php/utility/MakeOptions.php +2 -1
  78. package/project/.sdk/tm/py/pkg/feature/test_feature.py +15 -4
  79. package/project/.sdk/tm/py/pkg/utility/make_options.py +2 -0
  80. package/project/.sdk/tm/py/test/test_feature.py +40 -0
  81. package/project/.sdk/tm/py/test/test_feature_corpus.py +14 -3
  82. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -4
  83. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +15 -1
  84. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +3 -1
  85. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +20 -0
  86. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +13 -3
  87. package/project/.sdk/tm/ts/test/feature.test.ts +19 -9
  88. package/project/sdkgen-package.json +1 -1
  89. package/src/cmp/FeatureDocs.ts +100 -0
  90. package/src/cmp/Readme.ts +2 -0
  91. package/src/cmp/ReadmeFeatures.ts +113 -0
  92. package/src/cmp/ReadmeRef.ts +4 -0
  93. package/src/cmp/ReadmeRefFeatures.ts +149 -0
  94. package/src/cmp/ReadmeTop.ts +48 -0
  95. package/src/helpers/opShape.ts +5 -1
  96. package/src/sdkgen.ts +28 -7
  97. package/src/utility.ts +21 -1
@@ -17,6 +17,18 @@ import {
17
17
  // generated provider is verified without a server.
18
18
 
19
19
 
20
+ // Does this entity's load op have a real identifying param (path or
21
+ // required query), e.g. GET /result?trace_id=? A paramless GET has none.
22
+ function loadHasKey(ent: any): boolean {
23
+ const point = (ent.op && ent.op.load && ent.op.load.points || [])[0]
24
+ if (null == point) return false
25
+ const hasPathParam = (point.parts || []).some((p: string) => p.startsWith('{'))
26
+ const hasQueryParam = (point.args && point.args.query || [])
27
+ .some((q: any) => false !== q.reqd)
28
+ return hasPathParam || hasQueryParam
29
+ }
30
+
31
+
20
32
  // The name of the entity a parent path param addresses, or '' when the model
21
33
  // has none of that name.
22
34
  //
@@ -458,7 +470,12 @@ describe('${provider.fileBase}', () => {
458
470
  )
459
471
  })
460
472
 
461
-
473
+ `)
474
+ // Paramless read (e.g. GET /usage): every id "misses" the same
475
+ // way a hit does -- the mock has nothing to filter by -- so a
476
+ // load-missing test would just assert the happy path again.
477
+ if (loadHasKey(e.ent)) {
478
+ Content(`
462
479
  // A 404 from a single-item read is an ordinary "not found" answer, not a
463
480
  // failure: the provider turns it into null rather than letting the SDK
464
481
  // throw.
@@ -472,6 +489,7 @@ describe('${provider.fileBase}', () => {
472
489
  })
473
490
 
474
491
  `)
492
+ }
475
493
  }
476
494
  })
477
495
 
@@ -4,7 +4,7 @@ import {
4
4
  entityCollection, entityOps, entityIdField, entityClassName,
5
5
  opRequestShape, opParams, ownPoint, entityPath,
6
6
  collectDeps, repoInfo, packageName, packageVersion, apiName, envName,
7
- authorInfo, contributorList, isAuthActive, jsKey, jsProp,
7
+ authorInfo, contributorList, isAuthActive, isHttpBasicAuth, jsKey, jsProp,
8
8
  SdkGenError,
9
9
  PUBLISHER, PUBLISHER_URL,
10
10
  } from '@voxgig/sdkgen'
@@ -108,6 +108,14 @@ function recordKey(ent: any): string {
108
108
  if (null != lastParam) {
109
109
  return lastParam.slice(1, -1)
110
110
  }
111
+
112
+ // No path param at all (e.g. GET /scan/async/result?trace_id=...): the
113
+ // record's own key can still be a single required QUERY param.
114
+ const query = (point && point.args && point.args.query) || []
115
+ const reqdQuery = query.filter((q: any) => false !== q.reqd)
116
+ if (1 === reqdQuery.length) {
117
+ return String(reqdQuery[0].name)
118
+ }
111
119
  }
112
120
 
113
121
  return 'id'
@@ -462,6 +470,13 @@ const Main = cmp(function Main(props: any) {
462
470
  // model and strips the authorization header regardless of options, so
463
471
  // plumbing one anyway produces a credential path that cannot work.
464
472
  authActive: isAuthActive(model),
473
+ // Whether this API's scheme is genuine HTTP Basic Auth (two credentials,
474
+ // base64-joined), matching the SDK's own auth.basic signal. Decides
475
+ // whether the plugin also plumbs a `secret` alongside `apikey` — a
476
+ // Basic-Auth SDK's own auth stage deletes the header when either is
477
+ // missing, so a provider forwarding apikey alone could never
478
+ // authenticate, the same class of gap `apikey`-only forwarding was.
479
+ authBasic: isAuthActive(model) && isHttpBasicAuth(model),
465
480
  }
466
481
 
467
482
  // `.gitignore` is EMITTED rather than copied — npm strips that filename
@@ -895,7 +910,17 @@ ${provider.authActive ? `
895
910
  if (null != apikey && '' !== apikey) {
896
911
  sdkopts.apikey = apikey
897
912
  }
898
- ` : `
913
+ ${provider.authBasic ? `
914
+ // Genuine HTTP Basic Auth needs a SECOND credential (the SDK sends
915
+ // \`Authorization: Basic base64(apikey:secret)\`) — without it the SDK's
916
+ // auth stage treats the pair as incomplete and deletes the header, same
917
+ // as a missing apikey.
918
+ const secret = res?.keymap?.secret?.value
919
+
920
+ if (null != secret && '' !== secret) {
921
+ sdkopts.secret = secret
922
+ }
923
+ ` : ''}` : `
899
924
  // This API declares no authentication, so no credential is plumbed. The
900
925
  // SDK's auth stage emits nothing for an auth-inactive model and deletes
901
926
  // any \`authorization\` header regardless of options, so a keymap lookup
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { cmp, each, Content, canonToType, canonKey, canonScalarKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -376,6 +377,10 @@ let client = ${SDK}(options)
376
377
  \`\`\`
377
378
 
378
379
  `)
380
+ // The shared feature reference: options, defaults, usage and the
381
+ // considerations. Model facts, identical in every target, so they are
382
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
383
+ ReadmeRefFeatures({ target })
379
384
  }
380
385
 
381
386
  })
@@ -15,6 +15,7 @@ import {
15
15
  indent,
16
16
  isAuthActive,
17
17
  isConfigData,
18
+ isHttpBasicAuth,
18
19
  resolveAuthPrefix,
19
20
  serverVariables,
20
21
  } from '@voxgig/sdkgen'
@@ -49,9 +50,11 @@ const Config = cmp(async function Config(props: any) {
49
50
  const authActive = isAuthActive(model)
50
51
  // config.auth.prefix override -> spec-derived info.security.prefix -> 'Bearer'
51
52
  const authPrefix = resolveAuthPrefix(model)
53
+ const authBasic = isHttpBasicAuth(model)
52
54
  const authBlock = authActive
53
55
  ? `auth: {
54
- prefix: '${authPrefix}',
56
+ prefix: '${authPrefix}',${authBasic ? `
57
+ basic: true,` : ''}
55
58
  },
56
59
 
57
60
  `
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, envName, entityIdField, entityDataIdField, pickExampleEntity, opRequestShape, safeVarName, exampleVarName, jsKey } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, isHttpBasicAuth, envName, entityIdField, entityDataIdField, pickExampleEntity, opRequestShape, safeVarName, exampleVarName, jsKey } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -92,11 +92,16 @@ ${stateDataLine}
92
92
  : ''
93
93
 
94
94
  const authActive = isAuthActive(model)
95
+ const authBasic = authActive && isHttpBasicAuth(model)
95
96
  const apikeyTesterCtor = authActive
96
- ? `new ${model.const.Name}SDK({ apikey: '...' })`
97
+ ? `new ${model.const.Name}SDK({ apikey: '...'${authBasic ? `, secret: '...'` : ''} })`
97
98
  : `new ${model.const.Name}SDK()`
98
- const apikeyExtendField = authActive ? `\n apikey: '...',` : ''
99
- const apikeyEnvLine = authActive ? `\n${envName(model)}_APIKEY=<your-key>` : ''
99
+ const apikeyExtendField = authActive
100
+ ? `\n apikey: '...',${authBasic ? `\n secret: '...',` : ''}`
101
+ : ''
102
+ const apikeyEnvLine = authActive
103
+ ? `\n${envName(model)}_APIKEY=<your-key>${authBasic ? `\n${envName(model)}_SECRET=<your-secret>` : ''}`
104
+ : ''
100
105
 
101
106
  Content(`### Make a direct HTTP request
102
107
 
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, isHttpBasicAuth } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -51,17 +51,22 @@ const ReadmeModel = cmp(function ReadmeModel(props: any) {
51
51
  const returnBullets = retBullets.join('\n')
52
52
 
53
53
  const authActive = isAuthActive(model)
54
+ const authBasic = authActive && isHttpBasicAuth(model)
54
55
  const apikeyOptionType = authActive ? `\n apikey?: string` : ''
56
+ const secretOptionType = authBasic ? `\n secret?: string` : ''
55
57
  const apikeyOptionRow = authActive
56
58
  ? '| `apikey` | `string` | API key for authentication. |\n'
57
59
  : ''
60
+ const secretOptionRow = authBasic
61
+ ? '| `secret` | `string` | API secret for authentication. |\n'
62
+ : ''
58
63
 
59
64
  Content(`### ${model.const.Name}SDK
60
65
 
61
66
  #### Constructor
62
67
 
63
68
  \`\`\`ts
64
- new ${model.const.Name}SDK(options?: {${apikeyOptionType}
69
+ new ${model.const.Name}SDK(options?: {${apikeyOptionType}${secretOptionType}
65
70
  base?: string
66
71
  prefix?: string
67
72
  suffix?: string
@@ -72,7 +77,7 @@ new ${model.const.Name}SDK(options?: {${apikeyOptionType}
72
77
 
73
78
  | Option | Type | Description |
74
79
  | --- | --- | --- |
75
- ${apikeyOptionRow}| \`base\` | \`string\` | Base URL of the API server. |
80
+ ${apikeyOptionRow}${secretOptionRow}| \`base\` | \`string\` | Base URL of the API server. |
76
81
  | \`prefix\` | \`string\` | URL path prefix prepended to all requests. |
77
82
  | \`suffix\` | \`string\` | URL path suffix appended to all requests. |
78
83
  | \`feature\` | \`object\` | Feature activation flags (e.g. \`{ test: { active: true } }\`). |
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, each, Content, isAuthActive, packageName, envName, opRequestShape, entityIdField, entityDataIdField, entityOps, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, isAuthActive, isHttpBasicAuth, packageName, envName, opRequestShape, entityIdField, entityDataIdField, entityOps, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -41,7 +41,8 @@ const ReadmeQuick = cmp(function ReadmeQuick(props: any) {
41
41
  ) as any
42
42
 
43
43
  const ctor = isAuthActive(model)
44
- ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,\n})`
44
+ ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,${
45
+ isHttpBasicAuth(model) ? `\n secret: process.env.${envName(model)}_SECRET,` : ''}\n})`
45
46
  : `new ${model.const.Name}SDK()`
46
47
 
47
48
  Content(`### 1. Create a client
@@ -1,5 +1,6 @@
1
1
 
2
- import { cmp, each, Content, canonToType, File, isAuthActive, entityIdField, entityActions, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, each, Content, canonToType, File, isAuthActive, isHttpBasicAuth, entityIdField, entityActions, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -83,7 +84,7 @@ Create a new SDK client instance.
83
84
  | Name | Type | Description |
84
85
  | --- | --- | --- |
85
86
  | \`options\` | \`object\` | SDK configuration options. |
86
- ${isAuthActive(model) ? '| \`options.apikey\` | \`string\` | API key for authentication. |\n' : ''}| \`options.base\` | \`string\` | Base URL for API requests. |
87
+ ${isAuthActive(model) ? '| \`options.apikey\` | \`string\` | API key for authentication. |\n' : ''}${isAuthActive(model) && isHttpBasicAuth(model) ? '| \`options.secret\` | \`string\` | API secret for authentication. |\n' : ''}| \`options.base\` | \`string\` | Base URL for API requests. |
87
88
  | \`options.prefix\` | \`string\` | URL prefix appended after base. |
88
89
  | \`options.suffix\` | \`string\` | URL suffix appended after path. |
89
90
  | \`options.headers\` | \`object\` | Custom headers for all requests. |
@@ -454,6 +455,10 @@ const client = new ${model.Name}SDK({
454
455
  \`\`\`
455
456
 
456
457
  `)
458
+ // The shared feature reference: options, defaults, usage and the
459
+ // considerations. Model facts, identical in every target, so they are
460
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
461
+ ReadmeRefFeatures({ target })
457
462
  }
458
463
 
459
464
  })
@@ -1,5 +1,5 @@
1
1
 
2
- import { cmp, Content, isAuthActive, packageName, envName, entityIdField, entityOps, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
2
+ import { cmp, Content, isAuthActive, isHttpBasicAuth, packageName, envName, entityIdField, entityOps, opRequestShape, safeVarName, exampleVarName, jsKey, matchArg, idLiteral } from '@voxgig/sdkgen'
3
3
 
4
4
  import {
5
5
  KIT,
@@ -29,7 +29,8 @@ const ReadmeTopQuick = cmp(function ReadmeTopQuick(props: any) {
29
29
 
30
30
  const authActive = isAuthActive(model)
31
31
  const ctor = authActive
32
- ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,\n})`
32
+ ? `new ${model.const.Name}SDK({\n apikey: process.env.${envName(model)}_APIKEY,${
33
+ isHttpBasicAuth(model) ? `\n secret: process.env.${envName(model)}_SECRET,` : ''}\n})`
33
34
  : `new ${model.const.Name}SDK()`
34
35
 
35
36
  Content(`\`\`\`ts
@@ -17,6 +17,7 @@ import {
17
17
  cmp,
18
18
  snakify,
19
19
  isAuthActive,
20
+ isHttpBasicAuth,
20
21
  jsProp,
21
22
  jsOptProp, envName, envToken, liveStrict
22
23
  } from '@voxgig/sdkgen'
@@ -44,12 +45,14 @@ const TestDirect = cmp(function TestDirect(props: any) {
44
45
  const entidEnvVar = `${PROJECTNAME}_TEST_${envToken(entity.name)}_ENTID`
45
46
 
46
47
  const authActive = isAuthActive(model)
48
+ const authBasic = authActive && isHttpBasicAuth(model)
47
49
  const apikeyEnvEntry = authActive
48
- ? `\n '${PROJECTNAME}_APIKEY': 'NONE',`
50
+ ? `\n '${PROJECTNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJECTNAME}_SECRET': 'NONE',` : ''}`
49
51
  : ''
50
52
  const apikeyLiveField = authActive
51
53
  ? `
52
- apikey: env.${PROJECTNAME}_APIKEY,`
54
+ apikey: env.${PROJECTNAME}_APIKEY,${authBasic ? `
55
+ secret: env.${PROJECTNAME}_SECRET,` : ''}`
53
56
  : ''
54
57
 
55
58
  const opnames = Object.keys(entity.op || {})
@@ -30,6 +30,7 @@ import {
30
30
  cmp,
31
31
  each,
32
32
  isAuthActive,
33
+ isHttpBasicAuth,
33
34
  entityDataIdField, envName, envToken
34
35
  } from '@voxgig/sdkgen'
35
36
 
@@ -64,12 +65,14 @@ const TestEntity = cmp(function TestEntity(props: any) {
64
65
  const PROJENVNAME = envName(model)
65
66
  const ENTENVNAME = envToken(entity.name)
66
67
  const authActive = isAuthActive(model)
68
+ const authBasic = authActive && isHttpBasicAuth(model)
67
69
  const apikeyEnvEntry = authActive
68
- ? `\n '${PROJENVNAME}_APIKEY': 'NONE',`
70
+ ? `\n '${PROJENVNAME}_APIKEY': 'NONE',${authBasic ? `\n '${PROJENVNAME}_SECRET': 'NONE',` : ''}`
69
71
  : ''
70
72
  const apikeyLiveField = authActive
71
73
  ? `
72
- apikey: env.${PROJENVNAME}_APIKEY,`
74
+ apikey: env.${PROJENVNAME}_APIKEY,${authBasic ? `
75
+ secret: env.${PROJENVNAME}_SECRET,` : ''}`
73
76
  : ''
74
77
 
75
78
  // TODO: should be a utility function
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { cmp, each, Content, canonKey, canonScalarKey, File, isAuthActive, entityIdField, opRequestShape } from '@voxgig/sdkgen'
3
+ import { ReadmeRefFeatures } from '@voxgig/sdkgen'
3
4
 
4
5
  import {
5
6
  KIT,
@@ -389,6 +390,10 @@ const client = sdk.${model.const.Name}SDK.new(h.jo(&.{
389
390
  \`\`\`
390
391
 
391
392
  `)
393
+ // The shared feature reference: options, defaults, usage and the
394
+ // considerations. Model facts, identical in every target, so they are
395
+ // written once in cmp/ReadmeRefFeatures.ts rather than here.
396
+ ReadmeRefFeatures({ target })
392
397
  }
393
398
 
394
399
  })
@@ -619,6 +619,180 @@
619
619
  )
620
620
  fa))
621
621
 
622
+ ;; ---------------------------------------------------------------------------
623
+ ;; cost
624
+ ;; ---------------------------------------------------------------------------
625
+ ;; Prices every transport ATTEMPT and commits the spend once per OPERATION.
626
+ ;; Mirrors tm/ts/src/feature/cost/CostFeature.ts; the cases are
627
+ ;; .sdk/test/feature/cost.aon.
628
+ ;;
629
+ ;; ORDER MATTERS. Cost must sit INSIDE the cache, or a response served from
630
+ ;; cache is charged for money that was never spent. Activate in list form with
631
+ ;; cost first: [{name "cost"} {name "cache"}].
632
+
633
+ (defn cost-feature []
634
+ (let [fa (new-feature "cost" false "0.0.1")
635
+ per-unit (fn [] (let [p (opt fa "perUnit")] (if (number? p) p 0)))
636
+ budget-limit (fn [] (let [b (opt fa "budget")] (if (number? b) b 0)))
637
+ record! (fn []
638
+ (track-ensure! fa "_cost"
639
+ (vs/jm "currency" (or (opt fa "currency") "USD")
640
+ "total" (vs/jm "calls" 0 "attempts" 0 "amount" 0 "reported" 0 "estimated" 0)
641
+ "ops" (vs/jm) "actors" (vs/jm)
642
+ "budget" (vs/jm "limit" (budget-limit) "spent" 0
643
+ "remaining" (budget-limit) "exceeded" false)
644
+ "last" nil)))
645
+ bump! (fn [m k by] (.put ^java.util.Map m k (+ (or (mget m k) 0) by)))
646
+ ;; Accumulated per context, committed once at PreDone: adding each
647
+ ;; attempt to the running total and subtracting it again when a body
648
+ ;; figure supersedes it loses precision to catastrophic cancellation.
649
+ pending (fn [ctx]
650
+ (let [id (core/oget ctx :id)]
651
+ (or (get (:pending @fa) id)
652
+ (let [p (vs/jm "attempts" 0 "amount" 0 "reported" 0 "estimated" 0
653
+ "source" "none" "piped" false)]
654
+ (swap! fa update :pending assoc id p) p))))
655
+ price-header (fn [res]
656
+ (let [name (opt fa "header")]
657
+ (when (and (string? name) (seq name))
658
+ (let [v (header-of (vs/getprop res "headers") name)
659
+ n (to-num v)]
660
+ (when (number? n) [(* n (per-unit)) "header"])))))
661
+ ;; Same lookup grammar as rbac's rules: '<entity>.<op>', then '<op>',
662
+ ;; then '*'.
663
+ price-rate (fn [ctx]
664
+ (let [rates (let [r (opt fa "rates")] (if (vs/ismap r) r (vs/jm)))
665
+ op (core/oget ctx :op)
666
+ ent (or (when-let [e (core/oget ctx :entity)] (core/entity-get-name e))
667
+ (when op (core/op-entity op)) "")
668
+ opname (if op (core/op-name op) "")
669
+ pick (fn [k] (let [v (vs/getprop rates k)] (when (number? v) v)))]
670
+ (when-let [n (or (pick (str ent "." opname)) (pick opname) (pick "*"))]
671
+ [n "table"])))
672
+ price (fn [ctx res]
673
+ (or (price-header res)
674
+ (price-rate ctx)
675
+ (let [u (opt fa "unit")]
676
+ (if (and (number? u) (not= 0 u)) [u "unit"] [0 "none"]))))
677
+ ;; A usage figure from the parsed result body, priced by perUnit. Read
678
+ ;; at commit, not at the transport seam, because the body is one-shot.
679
+ price-body (fn [ctx]
680
+ (let [path (opt fa "path")]
681
+ (when (and (string? path) (seq path))
682
+ (when-let [result (core/oget ctx :result)]
683
+ (let [body (core/oget result :body)]
684
+ (when (vs/ismap body)
685
+ (let [n (to-num (vs/getpath body path))]
686
+ (when (number? n) (* n (per-unit))))))))))
687
+ spend! (fn [rec amount reported estimated]
688
+ (let [total (mget rec "total") budget (mget rec "budget")
689
+ lim (or (mget budget "limit") 0)]
690
+ (bump! total "amount" amount)
691
+ (bump! total "reported" reported)
692
+ (bump! total "estimated" estimated)
693
+ (let [spent (or (mget total "amount") 0)]
694
+ (.put ^java.util.Map budget "spent" spent)
695
+ (.put ^java.util.Map budget "remaining"
696
+ (if (> lim 0) (max 0 (- lim spent)) 0))
697
+ (when (and (> lim 0) (>= spent lim))
698
+ (.put ^java.util.Map budget "exceeded" true)))))
699
+ bump-bucket! (fn [bucket key amount]
700
+ (let [b (let [x (vs/getprop bucket key)]
701
+ (if (vs/ismap x) x
702
+ (let [m (vs/jm "calls" 0 "amount" 0)]
703
+ (.put ^java.util.Map bucket key m) m)))]
704
+ (bump! b "calls" 1)
705
+ (bump! b "amount" amount)))
706
+ commit! (fn [ctx p entity opname]
707
+ (let [rec (record!)
708
+ body (price-body ctx)
709
+ amount (if (some? body) body (or (mget p "amount") 0))
710
+ reported (if (some? body) body (or (mget p "reported") 0))
711
+ estimated (if (some? body) 0 (or (mget p "estimated") 0))
712
+ source (if (some? body) "body" (mget p "source"))
713
+ actor (or (core/oget (core/oget ctx :ctrl) :actor)
714
+ (opt fa "actor") "anonymous")]
715
+ (spend! rec amount reported estimated)
716
+ (bump! (mget rec "total") "calls" 1)
717
+ (bump-bucket! (mget rec "ops") (str entity "." opname) amount)
718
+ (bump-bucket! (mget rec "actors") (str actor) amount)
719
+ (swap! fa update :seq (fnil inc 0))
720
+ (.put ^java.util.Map rec "last"
721
+ (vs/jm "seq" (:seq @fa) "entity" entity "op" opname "actor" (str actor)
722
+ "amount" amount "currency" (mget rec "currency")
723
+ "source" source "attempts" (mget p "attempts")))))
724
+ finish! (fn [ctx done]
725
+ (when (active? fa)
726
+ (let [id (core/oget ctx :id) p (get (:pending @fa) id)]
727
+ (when (some? p)
728
+ (swap! fa update :pending dissoc id)
729
+ ;; A FAILED operation that made no attempt never reached
730
+ ;; the network - the budget gate refused it - and must
731
+ ;; not be counted. A SUCCEEDED one that made no attempt
732
+ ;; was served from the cache: a real call that cost
733
+ ;; nothing, which is the point of ordering cost inside
734
+ ;; the cache.
735
+ (when (or done (> (or (mget p "attempts") 0) 0))
736
+ (let [op (core/oget ctx :op)
737
+ entity (if (and op (seq (core/op-entity op))) (core/op-entity op) "_")
738
+ opname (if (and op (seq (core/op-name op))) (core/op-name op) "_")]
739
+ (commit! ctx p entity opname)))))))
740
+ charge (fn [ctx url fetchdef inner]
741
+ ;; A rejecting transport still costs an attempt: without this a
742
+ ;; run of connection-level failures under retry would be
743
+ ;; charged nothing, and an onBudget "deny" ceiling could never
744
+ ;; stop it.
745
+ (let [[res err] (inner ctx url fetchdef)
746
+ [amount source] (price ctx res)
747
+ p (pending ctx)]
748
+ (bump! p "attempts" 1)
749
+ (bump! p "amount" amount)
750
+ (bump! p (if (or (= source "header") (= source "body")) "reported" "estimated") amount)
751
+ (.put ^java.util.Map p "source" source)
752
+ (bump! (mget (record!) "total") "attempts" 1)
753
+ ;; direct() and graphql() dispatch no pipeline hooks, so
754
+ ;; their spend is committed here or never. `piped` is set by
755
+ ;; PrePoint, so its absence is the signal.
756
+ (when (not= true (mget p "piped"))
757
+ (swap! fa update :pending dissoc (core/oget ctx :id))
758
+ (commit! ctx p "_" "direct"))
759
+ [res err]))]
760
+ (swap! fa assoc :pending {} :seq 0)
761
+ (swap! fa assoc
762
+ "init" (fn [ctx options]
763
+ (swap! fa assoc :client (core/oget ctx :client)
764
+ :options (if (vs/ismap options) options (vs/jm))
765
+ :active (opts-active? options) :pending {} :seq 0)
766
+ (when (active? fa)
767
+ (record!)
768
+ (wrap-fetcher! ctx (fn [fctx url fd inner] (charge fctx url fd inner)))))
769
+ "PrePoint"
770
+ (fn [ctx]
771
+ (when (active? fa)
772
+ ;; Mark the context as piped, so charge knows a PreDone follows.
773
+ (.put ^java.util.Map (pending ctx) "piped" true)
774
+ (let [lim (budget-limit)]
775
+ (when (> lim 0)
776
+ (let [rec (record!)
777
+ spent (or (mget (mget rec "total") "amount") 0)]
778
+ (when (>= spent lim)
779
+ (.put ^java.util.Map (mget rec "budget") "exceeded" true)
780
+ (when (= "deny" (opt fa "onBudget"))
781
+ (let [err (core/ctx-error ctx "cost_budget"
782
+ (str "Cost budget of " lim " " (mget rec "currency")
783
+ " is spent (" spent " used)"))]
784
+ (core/out-set! ctx "point" err)
785
+ err))))))))
786
+ "PreDone" (fn [ctx] (finish! ctx true))
787
+ ;; A failed operation never reaches PreDone - make-error dispatches
788
+ ;; PreUnexpected instead - so without this its attempts are priced
789
+ ;; and then discarded: repeated connection failures would slip past
790
+ ;; an onBudget "deny" ceiling and leak the pending entry. `false`
791
+ ;; keeps a call that made NO attempt (refused at the budget gate)
792
+ ;; from being counted.
793
+ "PreUnexpected" (fn [ctx] (finish! ctx false)))
794
+ fa))
795
+
622
796
  ;; ---------------------------------------------------------------------------
623
797
  ;; streaming
624
798
  ;; ---------------------------------------------------------------------------
@@ -1108,4 +1282,5 @@
1108
1282
  "clienttrack" (clienttrack-feature)
1109
1283
  "rbac" (rbac-feature)
1110
1284
  "netsim" (netsim-feature)
1285
+ "cost" (cost-feature)
1111
1286
  (base-feature)))
@@ -134,6 +134,12 @@ void tests() {
134
134
  if (hasFeature('retry')) {
135
135
  describe('retry', () {
136
136
  test('retries transient failures then succeeds', (t) async {
137
+ // Drives netsim as the simulated network: runnable only when this
138
+ // SDK was generated with it (the harness skips absent features).
139
+ if (!hasFeature('netsim')) {
140
+ t.skip('this SDK was generated without the netsim feature');
141
+ return;
142
+ }
137
143
  final clock = makeClock();
138
144
  final h = makeClient(features: [
139
145
  {'name': 'netsim', 'options': {'failTimes': 2, 'failStatus': 503}},
@@ -152,6 +158,12 @@ void tests() {
152
158
  });
153
159
 
154
160
  test('gives up after the budget', (t) async {
161
+ // Drives netsim as the simulated network: runnable only when this
162
+ // SDK was generated with it (the harness skips absent features).
163
+ if (!hasFeature('netsim')) {
164
+ t.skip('this SDK was generated without the netsim feature');
165
+ return;
166
+ }
155
167
  final clock = makeClock();
156
168
  final h = makeClient(features: [
157
169
  {'name': 'netsim', 'options': {'failTimes': 9, 'failStatus': 500}},
@@ -203,6 +215,12 @@ void tests() {
203
215
  });
204
216
 
205
217
  test('honours a server Retry-After', (t) async {
218
+ // Drives netsim as the simulated network: runnable only when this
219
+ // SDK was generated with it (the harness skips absent features).
220
+ if (!hasFeature('netsim')) {
221
+ t.skip('this SDK was generated without the netsim feature');
222
+ return;
223
+ }
206
224
  final clock = makeClock();
207
225
  final h = makeClient(features: [
208
226
  {'name': 'netsim', 'options': {'rateLimitTimes': 1, 'retryAfter': 2}},
@@ -222,6 +240,12 @@ void tests() {
222
240
  });
223
241
 
224
242
  test('default jitter path still succeeds', (t) async {
243
+ // Drives netsim as the simulated network: runnable only when this
244
+ // SDK was generated with it (the harness skips absent features).
245
+ if (!hasFeature('netsim')) {
246
+ t.skip('this SDK was generated without the netsim feature');
247
+ return;
248
+ }
225
249
  final h = makeClient(features: [
226
250
  {'name': 'netsim', 'options': {'failTimes': 1}},
227
251
  {'name': 'retry', 'options': {'retries': 2, 'minDelay': 0}},
@@ -235,6 +259,12 @@ void tests() {
235
259
  if (hasFeature('timeout')) {
236
260
  describe('timeout', () {
237
261
  test('a slow request times out', (t) async {
262
+ // Drives netsim as the simulated network: runnable only when this
263
+ // SDK was generated with it (the harness skips absent features).
264
+ if (!hasFeature('netsim')) {
265
+ t.skip('this SDK was generated without the netsim feature');
266
+ return;
267
+ }
238
268
  final h = makeClient(features: [
239
269
  {'name': 'netsim', 'options': {'latency': 80}},
240
270
  {'name': 'timeout', 'options': {'ms': 10}},
@@ -470,6 +500,12 @@ void tests() {
470
500
  if (hasFeature('metrics')) {
471
501
  describe('metrics', () {
472
502
  test('counts ok and err per op', (t) async {
503
+ // Drives netsim as the simulated network: runnable only when this
504
+ // SDK was generated with it (the harness skips absent features).
505
+ if (!hasFeature('netsim')) {
506
+ t.skip('this SDK was generated without the netsim feature');
507
+ return;
508
+ }
473
509
  final h = makeClient(features: [
474
510
  {'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
475
511
  {'name': 'metrics', 'options': {}},
@@ -509,6 +545,12 @@ void tests() {
509
545
  });
510
546
 
511
547
  test('records a failed span on error', (t) async {
548
+ // Drives netsim as the simulated network: runnable only when this
549
+ // SDK was generated with it (the harness skips absent features).
550
+ if (!hasFeature('netsim')) {
551
+ t.skip('this SDK was generated without the netsim feature');
552
+ return;
553
+ }
512
554
  final h = makeClient(features: [
513
555
  {'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
514
556
  {'name': 'telemetry', 'options': {}},
@@ -542,6 +584,12 @@ void tests() {
542
584
  });
543
585
 
544
586
  test('captures failures', (t) async {
587
+ // Drives netsim as the simulated network: runnable only when this
588
+ // SDK was generated with it (the harness skips absent features).
589
+ if (!hasFeature('netsim')) {
590
+ t.skip('this SDK was generated without the netsim feature');
591
+ return;
592
+ }
545
593
  final h = makeClient(features: [
546
594
  {'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},
547
595
  {'name': 'debug', 'options': {}},
@@ -556,6 +604,12 @@ void tests() {
556
604
  if (hasFeature('audit')) {
557
605
  describe('audit', () {
558
606
  test('one record per op with sink + actor', (t) async {
607
+ // Drives netsim as the simulated network: runnable only when this
608
+ // SDK was generated with it (the harness skips absent features).
609
+ if (!hasFeature('netsim')) {
610
+ t.skip('this SDK was generated without the netsim feature');
611
+ return;
612
+ }
559
613
  final sink = [];
560
614
  final h = makeClient(features: [
561
615
  {'name': 'netsim', 'options': {'failTimes': 1, 'failStatus': 500}},