@voxgig/sdkgen 4.23.0 → 4.24.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.
- package/bin/voxgig-sdkgen +18 -2
- package/dist/action/doctor.d.ts +1 -1
- package/dist/action/doctor.js +12 -10
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +4 -0
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +5 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/remove.d.ts +16 -0
- package/dist/action/remove.js +345 -0
- package/dist/action/remove.js.map +1 -0
- package/dist/action/target.js +6 -1
- package/dist/action/target.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/tm/c/core/context.c +3 -1
- package/project/.sdk/tm/c/feature/paging.c +37 -3
- package/project/.sdk/tm/c/feature/test.c +51 -27
- package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
- package/project/.sdk/tm/c/utility/transform_request.c +25 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
- package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
- package/project/.sdk/tm/cpp/core/types.hpp +1 -1
- package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
- package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
- package/project/.sdk/tm/csharp/core/Context.cs +1 -1
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
- package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
- package/project/.sdk/tm/go/core/context.go +13 -13
- package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
- package/project/.sdk/tm/go/feature/test_feature.go +3 -15
- package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
- package/project/.sdk/tm/go/test/feature_test.go +54 -0
- package/project/.sdk/tm/go/utility/fetcher.go +41 -20
- package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
- package/project/.sdk/tm/go/utility/transform_request.go +22 -3
- package/project/.sdk/tm/java/core/Context.java +2 -1
- package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
- package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
- package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
- package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
- package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
- package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
- package/project/.sdk/tm/js/src/Context.js +7 -2
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
- package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
- package/project/.sdk/tm/lua/core/context.lua +8 -1
- package/project/.sdk/tm/lua/core/control.lua +3 -0
- package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
- package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
- package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
- package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
- package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
- package/project/.sdk/tm/perl/core/context.pm +2 -1
- package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
- package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
- package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
- package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
- package/project/.sdk/tm/php/core/Context.php +5 -1
- package/project/.sdk/tm/php/core/Control.php +5 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
- package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
- package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
- package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
- package/project/.sdk/tm/py/pkg/core/context.py +2 -1
- package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
- package/project/.sdk/tm/py/test/test_feature.py +27 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
- package/project/.sdk/tm/rb/core/context.rb +1 -1
- package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
- package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
- package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
- package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
- package/project/.sdk/tm/rust/core/context.rs +2 -6
- package/project/.sdk/tm/rust/feature/paging.rs +44 -3
- package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
- package/project/.sdk/tm/rust/feature/test.rs +66 -33
- package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
- package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
- package/project/.sdk/tm/scala/core/Context.scala +2 -1
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
- package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
- package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
- package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
- package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
- package/project/.sdk/tm/ts/src/Context.ts +7 -2
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
- package/project/.sdk/tm/zig/core/context.zig +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +19 -5
- package/project/.sdk/tm/zig/feature/paging.zig +34 -4
- package/project/.sdk/tm/zig/feature/test.zig +54 -26
- package/project/sdkgen-package.json +1 -1
- package/src/action/doctor.ts +12 -9
- package/src/action/edition.ts +8 -0
- package/src/action/feature.ts +7 -1
- package/src/action/remove.ts +492 -0
- package/src/action/target.ts +8 -1
|
@@ -35,8 +35,8 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
35
35
|
|
|
36
36
|
$this->client->mode = 'test';
|
|
37
37
|
|
|
38
|
-
// Ensure entity ids are correct.
|
|
39
|
-
\Voxgig\Struct\Struct::walk($entity_data, function ($key, $val, $parent, $path) {
|
|
38
|
+
// Ensure entity ids are correct (walk is by value: keep its result).
|
|
39
|
+
$entity_data = \Voxgig\Struct\Struct::walk($entity_data, function ($key, $val, $parent, $path) {
|
|
40
40
|
if (count($path) === 2 && is_array($val) && $key !== null) {
|
|
41
41
|
$val['id'] = $key;
|
|
42
42
|
}
|
|
@@ -103,9 +103,8 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
103
103
|
|
|
104
104
|
// PHP-portable equivalent of TS buildArgs+select: a flat-key
|
|
105
105
|
// filter that matches by exact-equality on each provided key,
|
|
106
|
-
// with alias fallback.
|
|
107
|
-
//
|
|
108
|
-
// create), list returns all entries.
|
|
106
|
+
// with alias fallback. An empty match matches every entry, as an
|
|
107
|
+
// empty `$AND` does in select; a non-empty miss matches nothing.
|
|
109
108
|
$find_first = function (array $entmap, $match, $alias) {
|
|
110
109
|
if (!is_array($match) || empty($match)) {
|
|
111
110
|
foreach ($entmap as $e) {
|
|
@@ -158,6 +157,23 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
158
157
|
|
|
159
158
|
$alias = is_object($op) ? ($op->alias ?? null) : \Voxgig\Struct\Struct::getprop($op, 'alias');
|
|
160
159
|
|
|
160
|
+
// Every op filters through buildArgs, which keeps `id` and the
|
|
161
|
+
// point's required params and drops the rest. find_first/find_all
|
|
162
|
+
// take a flat map, so take the primary key of each $OR clause;
|
|
163
|
+
// they do their own alias fallback.
|
|
164
|
+
$query_match = function ($args) use ($fctx, $op) {
|
|
165
|
+
$flat = [];
|
|
166
|
+
foreach (($this->buildArgs($fctx, $op, $args)['$AND'] ?? []) as $clause) {
|
|
167
|
+
$ors = $clause['$OR'] ?? [];
|
|
168
|
+
if (is_array($ors) && count($ors) > 0 && is_array($ors[0])) {
|
|
169
|
+
foreach ($ors[0] as $lk => $lv) {
|
|
170
|
+
$flat[$lk] = $lv;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return $flat;
|
|
175
|
+
};
|
|
176
|
+
|
|
161
177
|
// For single-entity ops (load, remove) with an empty explicit
|
|
162
178
|
// match, fall back to the id the entity client already knows from a
|
|
163
179
|
// prior create/load (carried in $fctx->match / $fctx->data). This
|
|
@@ -176,7 +192,7 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
176
192
|
};
|
|
177
193
|
|
|
178
194
|
if ($op->name === 'load') {
|
|
179
|
-
$ent = $find_first($entmap, $resolve_match($fctx->reqmatch), $alias);
|
|
195
|
+
$ent = $find_first($entmap, $query_match($resolve_match($fctx->reqmatch)), $alias);
|
|
180
196
|
if ($ent === null) {
|
|
181
197
|
return $respond(404, null, ['statusText' => 'Not found']);
|
|
182
198
|
}
|
|
@@ -185,37 +201,7 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
185
201
|
return $respond(200, $out);
|
|
186
202
|
|
|
187
203
|
} elseif ($op->name === 'list') {
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
// This branch used the whole of reqmatch, while load goes
|
|
191
|
-
// through resolve_match and the TS mock runs every op through
|
|
192
|
-
// buildArgs — which keeps `id` and the point's REQUIRED params
|
|
193
|
-
// and drops the rest. buildArgs is right here in this file and
|
|
194
|
-
// its docblock says it mirrors the TS one; list simply did not
|
|
195
|
-
// call it.
|
|
196
|
-
//
|
|
197
|
-
// So a match key that is neither `id` nor a required route
|
|
198
|
-
// param matched NOTHING in php and was IGNORED in ts. trello's
|
|
199
|
-
// board_star lists on `board_id`, which is not a field of that
|
|
200
|
-
// entity (id, pos, member_id, id_board) and not a route param:
|
|
201
|
-
// php returned an empty list and BoardStarEntityTest failed on
|
|
202
|
-
// an assertion that ts, js and every other port passed.
|
|
203
|
-
//
|
|
204
|
-
// buildArgs returns a struct query; find_all takes a flat map,
|
|
205
|
-
// so take the primary key of each $OR clause. find_all already
|
|
206
|
-
// does its own alias fallback, which is what the second $OR
|
|
207
|
-
// element carries.
|
|
208
|
-
$listargs = $this->buildArgs($fctx, $op, $fctx->reqmatch);
|
|
209
|
-
$listmatch = [];
|
|
210
|
-
foreach (($listargs['$AND'] ?? []) as $clause) {
|
|
211
|
-
$ors = $clause['$OR'] ?? [];
|
|
212
|
-
if (is_array($ors) && count($ors) > 0 && is_array($ors[0])) {
|
|
213
|
-
foreach ($ors[0] as $lk => $lv) {
|
|
214
|
-
$listmatch[$lk] = $lv;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
$found = $find_all($entmap, $listmatch, $alias);
|
|
204
|
+
$found = $find_all($entmap, $query_match($fctx->reqmatch), $alias);
|
|
219
205
|
$cleaned = [];
|
|
220
206
|
foreach ($found as $e) {
|
|
221
207
|
if (is_array($e)) unset($e['$KEY']);
|
|
@@ -245,14 +231,13 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
245
231
|
if (empty($update_match)) {
|
|
246
232
|
$update_match = $resolve_match([]);
|
|
247
233
|
}
|
|
248
|
-
$ent = $find_first($entmap, $update_match, $alias);
|
|
234
|
+
$ent = $find_first($entmap, $query_match($update_match), $alias);
|
|
249
235
|
if ($ent === null) {
|
|
236
|
+
// update miss: 404, never another record
|
|
250
237
|
return $respond(404, null, ['statusText' => 'Not found']);
|
|
251
238
|
}
|
|
252
239
|
if (is_array($fctx->reqdata)) {
|
|
253
|
-
|
|
254
|
-
$ent[$k] = $v;
|
|
255
|
-
}
|
|
240
|
+
$ent = \Voxgig\Struct\Struct::merge([$ent, $fctx->reqdata]);
|
|
256
241
|
}
|
|
257
242
|
$id = is_array($ent) ? ($ent['id'] ?? null) : null;
|
|
258
243
|
if ($id !== null) {
|
|
@@ -264,7 +249,7 @@ class ProjectNameTestFeature extends ProjectNameBaseFeature
|
|
|
264
249
|
return $respond(200, $out);
|
|
265
250
|
|
|
266
251
|
} elseif ($op->name === 'remove') {
|
|
267
|
-
$ent = $find_first($entmap, $resolve_match($fctx->reqmatch), $alias);
|
|
252
|
+
$ent = $find_first($entmap, $query_match($resolve_match($fctx->reqmatch)), $alias);
|
|
268
253
|
// Remove only the first matched entity. If nothing matches,
|
|
269
254
|
// succeed as a no-op rather than erroring.
|
|
270
255
|
$id = is_array($ent) ? ($ent['id'] ?? null) : null;
|
|
@@ -267,6 +267,19 @@ class FeatureCorpusTest extends TestCase
|
|
|
267
267
|
$this->assertSame($expect, $actual, $path);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// Whether the SDK built the feature at all. The activity record is the
|
|
271
|
+
// wrong probe: most features create theirs on first use, so an idle
|
|
272
|
+
// client has none and every section but the eager ones read as inert.
|
|
273
|
+
private static function present($client, string $name): bool
|
|
274
|
+
{
|
|
275
|
+
foreach ($client->features ?? [] as $f) {
|
|
276
|
+
if (($f->name ?? null) === $name) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
270
283
|
private static function record($client, string $name)
|
|
271
284
|
{
|
|
272
285
|
$prop = '_' . $name;
|
|
@@ -326,7 +339,10 @@ class FeatureCorpusTest extends TestCase
|
|
|
326
339
|
// Probed by ACTIVATING it: the feature defaults to inactive, so an
|
|
327
340
|
// idle client never builds it and its absence says nothing.
|
|
328
341
|
$probe = self::buildClient(['feature' => [['name' => $name, 'active' => true]]]);
|
|
329
|
-
if (
|
|
342
|
+
if (!self::present($probe, $name)) {
|
|
343
|
+
// The one line every runner prints for an inert section.
|
|
344
|
+
fwrite(STDERR, sprintf(
|
|
345
|
+
"feature.%s: inert (this SDK does not generate the feature)\n", $name));
|
|
330
346
|
continue;
|
|
331
347
|
}
|
|
332
348
|
|
|
@@ -1104,6 +1104,50 @@ class FeatureTest extends TestCase
|
|
|
1104
1104
|
$this->assertTrue($res['result']->paging['hasMore']);
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
|
+
public function test_paging_snake_case_signals_and_ctrl_write_back(): void
|
|
1108
|
+
{
|
|
1109
|
+
$this->needs('paging');
|
|
1110
|
+
$rec = new FtRecorder(function (int $n) {
|
|
1111
|
+
return FtHarness::response(200, 1 === $n
|
|
1112
|
+
? ['has_more' => true, 'next_cursor' => 'c2']
|
|
1113
|
+
: ['has_more' => false]);
|
|
1114
|
+
});
|
|
1115
|
+
$h = new FtHarness([
|
|
1116
|
+
['name' => 'paging'],
|
|
1117
|
+
], $rec->server());
|
|
1118
|
+
$res = $h->op(['op' => 'list', 'path' => '/w', 'ctrl' => ['paging' => []]]);
|
|
1119
|
+
$this->assertTrue($res['result']->paging['hasMore']);
|
|
1120
|
+
$this->assertSame('c2', $res['result']->paging['cursor']);
|
|
1121
|
+
$written = $res['ctx']->ctrl->paging;
|
|
1122
|
+
$this->assertSame('c2', $written['cursor']);
|
|
1123
|
+
$this->assertTrue($written['hasMore']);
|
|
1124
|
+
// Arrays are values here, so the written-back record is passed on by hand.
|
|
1125
|
+
$res = $h->op(['op' => 'list', 'path' => '/w', 'ctrl' => ['paging' => $written]]);
|
|
1126
|
+
$this->assertStringContainsString('cursor=c2', $rec->calls[1]['url']);
|
|
1127
|
+
$this->assertFalse($res['ctx']->ctrl->paging['hasMore']);
|
|
1128
|
+
$this->assertNull($res['ctx']->ctrl->paging['cursor']);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
public function test_paging_continues_from_written_back_next_page(): void
|
|
1132
|
+
{
|
|
1133
|
+
$this->needs('paging');
|
|
1134
|
+
$rec = new FtRecorder(function (int $n) {
|
|
1135
|
+
return FtHarness::response(200, 1 === $n ? ['next_page' => 2] : [],
|
|
1136
|
+
['x-page' => (string) $n]);
|
|
1137
|
+
});
|
|
1138
|
+
$h = new FtHarness([
|
|
1139
|
+
['name' => 'paging'],
|
|
1140
|
+
], $rec->server());
|
|
1141
|
+
$res = $h->op(['op' => 'list', 'path' => '/w']);
|
|
1142
|
+
$written = $res['ctx']->ctrl->paging;
|
|
1143
|
+
$this->assertEquals(1, $written['page']);
|
|
1144
|
+
$this->assertEquals(2, $written['nextPage']);
|
|
1145
|
+
$this->assertTrue($written['hasMore']);
|
|
1146
|
+
$res = $h->op(['op' => 'list', 'path' => '/w', 'ctrl' => ['paging' => $written]]);
|
|
1147
|
+
$this->assertStringContainsString('page=2', $rec->calls[1]['url']);
|
|
1148
|
+
$this->assertFalse($res['ctx']->ctrl->paging['hasMore']);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1107
1151
|
public function test_paging_non_list_op_is_not_paged(): void
|
|
1108
1152
|
{
|
|
1109
1153
|
$this->needs('paging');
|
|
@@ -12,6 +12,22 @@ class ProjectNameFetcher
|
|
|
12
12
|
// Users can override by passing a User-Agent header in fetchdef.
|
|
13
13
|
public const DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; ProjectNameSDK/1.0)';
|
|
14
14
|
|
|
15
|
+
// One cURL handle per process, reset between calls rather than closed:
|
|
16
|
+
// curl_reset keeps the connection cache, so the next request to the
|
|
17
|
+
// same host reuses the open connection.
|
|
18
|
+
private static $curl = null;
|
|
19
|
+
|
|
20
|
+
private static function curlHandle(string $fullurl)
|
|
21
|
+
{
|
|
22
|
+
if (self::$curl === null) {
|
|
23
|
+
self::$curl = curl_init();
|
|
24
|
+
} else {
|
|
25
|
+
curl_reset(self::$curl);
|
|
26
|
+
}
|
|
27
|
+
curl_setopt(self::$curl, CURLOPT_URL, $fullurl);
|
|
28
|
+
return self::$curl;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
public static function defaultHttpFetch(string $fullurl, array $fetchdef): array
|
|
16
32
|
{
|
|
17
33
|
$method_str = strtoupper($fetchdef['method'] ?? 'GET');
|
|
@@ -130,7 +146,7 @@ class ProjectNameFetcher
|
|
|
130
146
|
];
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
$ch =
|
|
149
|
+
$ch = self::curlHandle($fullurl);
|
|
134
150
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
135
151
|
curl_setopt($ch, CURLOPT_HEADER, false);
|
|
136
152
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
|
|
@@ -157,7 +173,6 @@ class ProjectNameFetcher
|
|
|
157
173
|
$response_body = curl_exec($ch);
|
|
158
174
|
if ($response_body === false) {
|
|
159
175
|
$err = curl_error($ch) ?: 'curl_exec failed';
|
|
160
|
-
curl_close($ch);
|
|
161
176
|
return [
|
|
162
177
|
[
|
|
163
178
|
'status' => 0,
|
|
@@ -169,7 +184,6 @@ class ProjectNameFetcher
|
|
|
169
184
|
$err,
|
|
170
185
|
];
|
|
171
186
|
}
|
|
172
|
-
curl_close($ch);
|
|
173
187
|
|
|
174
188
|
$status = 0;
|
|
175
189
|
$status_text = '';
|
|
@@ -22,7 +22,7 @@ class ProjectNamePrepareQuery
|
|
|
22
22
|
foreach ($items as $item) {
|
|
23
23
|
$key = $item[0];
|
|
24
24
|
$val = $item[1];
|
|
25
|
-
if ($val !== null && is_string($key) && !in_array($key, $params, true)) {
|
|
25
|
+
if ($val !== null && is_string($key) && '$action' !== $key && !in_array($key, $params, true)) {
|
|
26
26
|
$out[$key] = $val;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -16,12 +16,24 @@ class ProjectNameTransformRequest
|
|
|
16
16
|
}
|
|
17
17
|
$transform = ProjectNameHelpers::to_map(\Voxgig\Struct\Struct::getprop($point, 'transform'));
|
|
18
18
|
if (!$transform) {
|
|
19
|
-
return $ctx->reqdata;
|
|
19
|
+
return self::strip_action($ctx->reqdata);
|
|
20
20
|
}
|
|
21
21
|
$reqform = \Voxgig\Struct\Struct::getprop($transform, 'req');
|
|
22
22
|
if (!$reqform) {
|
|
23
|
-
return $ctx->reqdata;
|
|
23
|
+
return self::strip_action($ctx->reqdata);
|
|
24
24
|
}
|
|
25
|
-
return \Voxgig\Struct\Struct::transform(['reqdata' => $ctx->reqdata], $reqform);
|
|
25
|
+
return self::strip_action(\Voxgig\Struct\Struct::transform(['reqdata' => $ctx->reqdata], $reqform));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// `$action` selects the point (see MakePoint); it is never an API field,
|
|
29
|
+
// so the body is a copy without it. Arrays are values, so the unset
|
|
30
|
+
// never reaches the caller's copy.
|
|
31
|
+
private static function strip_action(mixed $reqdata): mixed
|
|
32
|
+
{
|
|
33
|
+
if (!is_array($reqdata) || !array_key_exists('$action', $reqdata)) {
|
|
34
|
+
return $reqdata;
|
|
35
|
+
}
|
|
36
|
+
unset($reqdata['$action']);
|
|
37
|
+
return $reqdata;
|
|
26
38
|
}
|
|
27
39
|
}
|
|
@@ -53,7 +53,8 @@ class ProjectNameContext:
|
|
|
53
53
|
self.ctrl.actor = ctrl_raw["actor"]
|
|
54
54
|
if isinstance(ctrl_raw.get("paging"), dict):
|
|
55
55
|
self.ctrl.paging = ctrl_raw["paging"]
|
|
56
|
-
elif basectx is not None and basectx.ctrl is not None
|
|
56
|
+
elif (basectx is not None and basectx.ctrl is not None
|
|
57
|
+
and get_ctx_prop(ctxmap, "opname") is None):
|
|
57
58
|
self.ctrl = basectx.ctrl
|
|
58
59
|
|
|
59
60
|
# Meta
|
|
@@ -12,8 +12,8 @@ from projectname_sdk.feature.base_feature import ProjectNameBaseFeature
|
|
|
12
12
|
# stamps page/limit (or a cursor) into the request query; on the way back
|
|
13
13
|
# (PreResult) it reads the server's pagination signals — a `Link:
|
|
14
14
|
# rel="next"` header, `X-Page`/`X-Next-Page`/`X-Total-Count` headers, or
|
|
15
|
-
# `next`/`cursor`/`nextCursor`/`hasMore` fields in the body
|
|
16
|
-
# them on `ctx.result.paging
|
|
15
|
+
# `next`/`cursor`/`nextCursor`/`hasMore` fields in the body (snake_case forms
|
|
16
|
+
# too) — and records them on `ctx.result.paging` and the caller's ctrl paging. A per-call cursor/page supplied via ctrl
|
|
17
17
|
# paging takes priority (used by auto-iteration). Parameter names
|
|
18
18
|
# (`pageParam`/`limitParam`/`cursorParam`), `startPage` and page size
|
|
19
19
|
# (`limit`) are configurable.
|
|
@@ -66,7 +66,11 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
|
|
|
66
66
|
if paging.get("cursor") is not None:
|
|
67
67
|
spec.query[cursor_param] = paging["cursor"]
|
|
68
68
|
elif spec.query.get(page_param) is None:
|
|
69
|
-
page
|
|
69
|
+
# A record written back by PreResult holds the page just fetched
|
|
70
|
+
# as "page" and the one to fetch as "nextPage", so nextPage wins.
|
|
71
|
+
page = paging.get("nextPage")
|
|
72
|
+
if page is None:
|
|
73
|
+
page = paging.get("page")
|
|
70
74
|
if page is None:
|
|
71
75
|
page = self.options.get("startPage") or 1
|
|
72
76
|
spec.query[page_param] = page
|
|
@@ -166,14 +170,25 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
|
|
|
166
170
|
paging["hasMore"] = more
|
|
167
171
|
explicit_more = True
|
|
168
172
|
|
|
169
|
-
# Body-level
|
|
173
|
+
# Body-level signals; snake_case forms are read first so camelCase wins.
|
|
170
174
|
if isinstance(body, dict):
|
|
171
175
|
if body.get("next") is not None:
|
|
172
176
|
paging["next"] = paging["next"] or body["next"]
|
|
177
|
+
if body.get("next_cursor") is not None:
|
|
178
|
+
paging["cursor"] = body["next_cursor"]
|
|
173
179
|
if body.get("cursor") is not None:
|
|
174
180
|
paging["cursor"] = body["cursor"]
|
|
175
181
|
if body.get("nextCursor") is not None:
|
|
176
182
|
paging["cursor"] = body["nextCursor"]
|
|
183
|
+
if paging["nextPage"] is None:
|
|
184
|
+
np = body.get("nextPage")
|
|
185
|
+
if np is None:
|
|
186
|
+
np = body.get("next_page")
|
|
187
|
+
if isinstance(np, (int, float, str)) and not isinstance(np, bool):
|
|
188
|
+
paging["nextPage"] = np
|
|
189
|
+
if isinstance(body.get("has_more"), bool):
|
|
190
|
+
paging["hasMore"] = body["has_more"]
|
|
191
|
+
explicit_more = True
|
|
177
192
|
if isinstance(body.get("hasMore"), bool):
|
|
178
193
|
paging["hasMore"] = body["hasMore"]
|
|
179
194
|
explicit_more = True
|
|
@@ -191,6 +206,16 @@ class ProjectNamePagingFeature(ProjectNameBaseFeature):
|
|
|
191
206
|
|
|
192
207
|
result.paging = paging
|
|
193
208
|
|
|
209
|
+
ctrl = ctx.ctrl
|
|
210
|
+
if ctrl is not None:
|
|
211
|
+
# The Context shares the caller's paging dict by reference, so
|
|
212
|
+
# refill it in place; a fresh dict on ctrl never reaches the caller.
|
|
213
|
+
if isinstance(getattr(ctrl, "paging", None), dict):
|
|
214
|
+
ctrl.paging.clear()
|
|
215
|
+
ctrl.paging.update(paging)
|
|
216
|
+
else:
|
|
217
|
+
ctrl.paging = paging
|
|
218
|
+
|
|
194
219
|
self.client._paging = {"last": paging}
|
|
195
220
|
|
|
196
221
|
def _is_list(self, ctx):
|
|
@@ -143,18 +143,11 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
|
|
|
143
143
|
args = test_self.build_args(fctx, op, update_match)
|
|
144
144
|
found = vs.select(entmap, args)
|
|
145
145
|
ent = vs.getelem(found, 0)
|
|
146
|
-
if ent is None and isinstance(entmap, dict):
|
|
147
|
-
for e in entmap.values():
|
|
148
|
-
if isinstance(e, dict):
|
|
149
|
-
ent = e
|
|
150
|
-
break
|
|
151
146
|
if ent is None:
|
|
147
|
+
# update miss: 404, never another record
|
|
152
148
|
return respond(404, None, {"statusText": "Not found"})
|
|
153
|
-
if isinstance(ent, dict):
|
|
154
|
-
|
|
155
|
-
if reqdata is not None:
|
|
156
|
-
for k, v in reqdata.items():
|
|
157
|
-
ent[k] = v
|
|
149
|
+
if isinstance(ent, dict) and isinstance(fctx.reqdata, dict):
|
|
150
|
+
vs.merge([ent, fctx.reqdata])
|
|
158
151
|
vs.delprop(ent, "$KEY")
|
|
159
152
|
out = vs.clone(ent)
|
|
160
153
|
return respond(200, out)
|
|
@@ -260,12 +253,36 @@ class ProjectNameTestFeature(ProjectNameBaseFeature):
|
|
|
260
253
|
|
|
261
254
|
return netsim_fetcher
|
|
262
255
|
|
|
256
|
+
# The entity's own endpoint: a terminal `{param}` marks a record route,
|
|
257
|
+
# and among equals the shallower path wins (the same rule as make_point).
|
|
258
|
+
@staticmethod
|
|
259
|
+
def pick_point(points):
|
|
260
|
+
if not isinstance(points, list) or len(points) == 0:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
def terminal(p):
|
|
264
|
+
parts = vs.getprop(p, "parts")
|
|
265
|
+
return isinstance(parts, list) and len(parts) > 0 and \
|
|
266
|
+
isinstance(parts[-1], str) and parts[-1].startswith("{")
|
|
267
|
+
|
|
268
|
+
def depth(p):
|
|
269
|
+
parts = vs.getprop(p, "parts")
|
|
270
|
+
return len(parts) if isinstance(parts, list) else 0
|
|
271
|
+
|
|
272
|
+
point = points[0]
|
|
273
|
+
for cand in points[1:]:
|
|
274
|
+
if terminal(cand) != terminal(point):
|
|
275
|
+
if terminal(cand):
|
|
276
|
+
point = cand
|
|
277
|
+
elif depth(cand) < depth(point):
|
|
278
|
+
point = cand
|
|
279
|
+
return point
|
|
280
|
+
|
|
263
281
|
def build_args(self, ctx, op, args):
|
|
264
282
|
opname = op.name
|
|
265
283
|
|
|
266
|
-
# Get last point from config.
|
|
267
284
|
points = vs.getpath(ctx.config, "entity." + ctx.entity.get_name() + ".op." + opname + ".points")
|
|
268
|
-
point =
|
|
285
|
+
point = self.pick_point(points)
|
|
269
286
|
|
|
270
287
|
# Path AND query: a path-only read misses a query-addressed record
|
|
271
288
|
# (e.g. GET /result?trace_id=), which has no path param at all.
|
|
@@ -2,21 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
import json
|
|
5
|
+
import threading
|
|
6
|
+
from http.cookiejar import DefaultCookiePolicy
|
|
5
7
|
from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
# Default User-Agent — many CDNs (notably Cloudflare) reject requests with
|
|
9
|
-
#
|
|
10
|
-
# the
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# a library's default UA, returning 403 before the request even reaches
|
|
12
|
+
# the origin. Set a Mozilla-shaped UA so the SDK behaves like every other
|
|
13
|
+
# HTTP client by default. Users can still override by passing a User-Agent
|
|
14
|
+
# header in fetchdef.
|
|
13
15
|
_DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; ProjectNameSDK/1.0)"
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
_SESSION = None
|
|
19
|
+
_SESSION_LOCK = threading.Lock()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _session():
|
|
23
|
+
# Pool connections without retaining response cookies between calls.
|
|
24
|
+
global _SESSION
|
|
25
|
+
if _SESSION is None:
|
|
26
|
+
import requests
|
|
27
|
+
with _SESSION_LOCK:
|
|
28
|
+
if _SESSION is None:
|
|
29
|
+
session = requests.Session()
|
|
30
|
+
session.cookies.set_policy(DefaultCookiePolicy(allowed_domains=[]))
|
|
31
|
+
_SESSION = session
|
|
32
|
+
return _SESSION
|
|
33
|
+
|
|
19
34
|
|
|
35
|
+
def _default_http_fetch(fullurl, fetchdef):
|
|
20
36
|
method = fetchdef.get("method", "GET")
|
|
21
37
|
body_str = fetchdef.get("body")
|
|
22
38
|
headers = fetchdef.get("headers", {})
|
|
@@ -26,76 +42,58 @@ def _default_http_fetch(fullurl, fetchdef):
|
|
|
26
42
|
|
|
27
43
|
data = body_str.encode("utf-8") if body_str is not None else None
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
req_headers = {}
|
|
30
46
|
has_ua = False
|
|
31
47
|
for k, v in headers.items():
|
|
32
48
|
if k.lower() == "user-agent":
|
|
33
49
|
has_ua = True
|
|
34
|
-
|
|
50
|
+
req_headers[k] = v
|
|
35
51
|
if not has_ua:
|
|
36
|
-
|
|
52
|
+
req_headers["User-Agent"] = _DEFAULT_USER_AGENT
|
|
37
53
|
|
|
38
54
|
# Manual redirects: fetchdef["redirect"] == "manual" surfaces a 3xx as
|
|
39
|
-
# an ordinary response instead of auto-following it
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
# an ordinary response instead of auto-following it, which would replay
|
|
56
|
+
# the request, headers included, against whatever host Location names.
|
|
57
|
+
# Set by the station feature's transport middleware under an egress
|
|
58
|
+
# hosts policy (mirrors the ts fetch option).
|
|
59
|
+
kwargs = {"allow_redirects": fetchdef.get("redirect") != "manual"}
|
|
60
|
+
|
|
61
|
+
proxy = fetchdef.get("proxy")
|
|
62
|
+
if isinstance(proxy, str) and proxy:
|
|
63
|
+
kwargs["proxies"] = {"http": proxy, "https": proxy}
|
|
64
|
+
|
|
65
|
+
timeout = fetchdef.get("timeout")
|
|
66
|
+
if isinstance(timeout, (int, float)) and timeout > 0:
|
|
67
|
+
kwargs["timeout"] = timeout
|
|
49
68
|
|
|
50
69
|
try:
|
|
51
|
-
resp =
|
|
52
|
-
body = resp.
|
|
53
|
-
resp_headers = {}
|
|
54
|
-
for k, v in resp.getheaders():
|
|
55
|
-
resp_headers[k.lower()] = v
|
|
56
|
-
|
|
57
|
-
json_body = None
|
|
58
|
-
if len(body) > 0:
|
|
59
|
-
try:
|
|
60
|
-
json_body = json.loads(body)
|
|
61
|
-
except Exception:
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
status = resp.getcode()
|
|
65
|
-
status_text = "OK" if status < 400 else "Error"
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
"status": status,
|
|
69
|
-
"statusText": status_text,
|
|
70
|
-
"headers": resp_headers,
|
|
71
|
-
"json": lambda: json_body,
|
|
72
|
-
"body": body,
|
|
73
|
-
}, None
|
|
74
|
-
except urllib.error.HTTPError as e:
|
|
75
|
-
body = e.read().decode("utf-8")
|
|
76
|
-
resp_headers = {}
|
|
77
|
-
for k, v in e.headers.items():
|
|
78
|
-
resp_headers[k.lower()] = v
|
|
79
|
-
|
|
80
|
-
json_body = None
|
|
81
|
-
if len(body) > 0:
|
|
82
|
-
try:
|
|
83
|
-
json_body = json.loads(body)
|
|
84
|
-
except Exception:
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
status_text = "OK" if e.code < 400 else "Error"
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
"status": e.code,
|
|
91
|
-
"statusText": status_text,
|
|
92
|
-
"headers": resp_headers,
|
|
93
|
-
"json": lambda: json_body,
|
|
94
|
-
"body": body,
|
|
95
|
-
}, None
|
|
70
|
+
resp = _session().request(method, fullurl, data=data, headers=req_headers, **kwargs)
|
|
71
|
+
body = resp.content.decode("utf-8")
|
|
96
72
|
except Exception as e:
|
|
97
73
|
return None, str(e)
|
|
98
74
|
|
|
75
|
+
resp_headers = {}
|
|
76
|
+
for k, v in resp.headers.items():
|
|
77
|
+
resp_headers[k.lower()] = v
|
|
78
|
+
|
|
79
|
+
json_body = None
|
|
80
|
+
if len(body) > 0:
|
|
81
|
+
try:
|
|
82
|
+
json_body = json.loads(body)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
status = resp.status_code
|
|
87
|
+
status_text = resp.reason or ("OK" if status < 400 else "Error")
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"status": status,
|
|
91
|
+
"statusText": status_text,
|
|
92
|
+
"headers": resp_headers,
|
|
93
|
+
"json": lambda: json_body,
|
|
94
|
+
"body": body,
|
|
95
|
+
}, None
|
|
96
|
+
|
|
99
97
|
|
|
100
98
|
def fetcher_util(ctx, fullurl, fetchdef):
|
|
101
99
|
if ctx.client.mode != "live":
|
|
@@ -27,7 +27,8 @@ def prepare_query_util(ctx):
|
|
|
27
27
|
for item in reqmatch_items:
|
|
28
28
|
key = item[0]
|
|
29
29
|
val = item[1]
|
|
30
|
-
if val is not None and isinstance(key, str) and
|
|
30
|
+
if val is not None and isinstance(key, str) and key != "$action" \
|
|
31
|
+
and not _contains_param(params, key):
|
|
31
32
|
out[key] = val
|
|
32
33
|
|
|
33
34
|
return out
|
|
@@ -5,6 +5,14 @@ from projectname_sdk.utility.voxgig_struct import voxgig_struct as vs
|
|
|
5
5
|
from projectname_sdk.core.helpers import to_map
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
# `$action` selects the point (see make_point_util); it is never an API field,
|
|
9
|
+
# so the body is a copy without it. The caller's dict is left untouched.
|
|
10
|
+
def _strip_action(reqdata):
|
|
11
|
+
if not isinstance(reqdata, dict) or "$action" not in reqdata:
|
|
12
|
+
return reqdata
|
|
13
|
+
return {k: v for k, v in reqdata.items() if k != "$action"}
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
def transform_request_util(ctx):
|
|
9
17
|
spec = ctx.spec
|
|
10
18
|
point = ctx.point
|
|
@@ -14,14 +22,14 @@ def transform_request_util(ctx):
|
|
|
14
22
|
|
|
15
23
|
transform = to_map(vs.getprop(point, "transform"))
|
|
16
24
|
if transform is None:
|
|
17
|
-
return ctx.reqdata
|
|
25
|
+
return _strip_action(ctx.reqdata)
|
|
18
26
|
|
|
19
27
|
reqform = vs.getprop(transform, "req")
|
|
20
28
|
if reqform is None:
|
|
21
|
-
return ctx.reqdata
|
|
29
|
+
return _strip_action(ctx.reqdata)
|
|
22
30
|
|
|
23
31
|
reqdata = vs.transform({
|
|
24
32
|
"reqdata": ctx.reqdata,
|
|
25
33
|
}, reqform)
|
|
26
34
|
|
|
27
|
-
return reqdata
|
|
35
|
+
return _strip_action(reqdata)
|