@chrismo/superkit 1.1.0 → 1.3.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/dist/recipes.test.d.ts +2 -0
- package/dist/recipes.test.d.ts.map +1 -0
- package/dist/recipes.test.js +142 -0
- package/dist/recipes.test.js.map +1 -0
- package/docs/recipes/character.spq +1 -1
- package/docs/recipes/escape.spq +1 -1
- package/docs/recipes/string.md +6 -6
- package/docs/recipes/string.spq +11 -12
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.test.d.ts","sourceRoot":"","sources":["../src/recipes.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { superRecipes } from './lib/recipes.js';
|
|
6
|
+
const recipesDir = join(import.meta.dirname, '..', 'docs', 'recipes');
|
|
7
|
+
// Load all .spq files and strip skdoc blocks — they contain nested
|
|
8
|
+
// brackets in example fields that super can't parse as valid syntax.
|
|
9
|
+
// Keep only the actual fn/op implementations.
|
|
10
|
+
const allSpqFiles = readdirSync(recipesDir)
|
|
11
|
+
.filter(f => f.endsWith('.spq'))
|
|
12
|
+
.sort();
|
|
13
|
+
// Strip skdoc blocks and functions that can't be parsed by super -I
|
|
14
|
+
// (sk_shell_quote has nested quotes in f-strings that break the file parser)
|
|
15
|
+
const UNPARSEABLE_FNS = [
|
|
16
|
+
'sk_shell_quote', // nested quotes in f-string
|
|
17
|
+
'sk_merge_records', // string literal with braces
|
|
18
|
+
'sk_chr', // uses `let` keyword (broken in current super)
|
|
19
|
+
'sk_alpha', // depends on sk_chr
|
|
20
|
+
'sk_seq', // depends on sk_chr (via sk_pad_left, but also broken)
|
|
21
|
+
'sk_add_ids', // uses `that` (old syntax for `this`)
|
|
22
|
+
];
|
|
23
|
+
function stripSkdocBlocks(content) {
|
|
24
|
+
const lines = content.split('\n');
|
|
25
|
+
const result = [];
|
|
26
|
+
let inSkdoc = false;
|
|
27
|
+
let inUnparseable = false;
|
|
28
|
+
let skipNextClosingParen = false;
|
|
29
|
+
let parenDepth = 0;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
// Strip skdoc metadata blocks
|
|
32
|
+
if (!inSkdoc && !inUnparseable && /^(?:fn|op)\s+skdoc_/.test(line)) {
|
|
33
|
+
inSkdoc = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (inSkdoc) {
|
|
37
|
+
if (line.includes('<skdoc>)')) {
|
|
38
|
+
inSkdoc = false;
|
|
39
|
+
skipNextClosingParen = true;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (skipNextClosingParen) {
|
|
44
|
+
if (line.trim() === '') {
|
|
45
|
+
result.push(line);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (line.trim() === ')') {
|
|
49
|
+
skipNextClosingParen = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
skipNextClosingParen = false;
|
|
53
|
+
}
|
|
54
|
+
// Strip functions that cause parse errors when loaded via -I
|
|
55
|
+
if (!inUnparseable) {
|
|
56
|
+
const fnMatch = line.match(/^(?:fn|op)\s+(\w+)/);
|
|
57
|
+
if (fnMatch && UNPARSEABLE_FNS.includes(fnMatch[1])) {
|
|
58
|
+
inUnparseable = true;
|
|
59
|
+
parenDepth = 0;
|
|
60
|
+
for (const ch of line) {
|
|
61
|
+
if (ch === '(')
|
|
62
|
+
parenDepth++;
|
|
63
|
+
if (ch === ')')
|
|
64
|
+
parenDepth--;
|
|
65
|
+
}
|
|
66
|
+
if (parenDepth <= 0)
|
|
67
|
+
inUnparseable = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (inUnparseable) {
|
|
72
|
+
for (const ch of line) {
|
|
73
|
+
if (ch === '(')
|
|
74
|
+
parenDepth++;
|
|
75
|
+
if (ch === ')')
|
|
76
|
+
parenDepth--;
|
|
77
|
+
}
|
|
78
|
+
if (parenDepth <= 0)
|
|
79
|
+
inUnparseable = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
result.push(line);
|
|
83
|
+
}
|
|
84
|
+
return result.join('\n');
|
|
85
|
+
}
|
|
86
|
+
import { writeFileSync } from 'fs';
|
|
87
|
+
import { tmpdir } from 'os';
|
|
88
|
+
const allDefinitions = allSpqFiles
|
|
89
|
+
.map(f => stripSkdocBlocks(readFileSync(join(recipesDir, f), 'utf-8')))
|
|
90
|
+
.join('\n');
|
|
91
|
+
function normalizeOutput(s) {
|
|
92
|
+
return s
|
|
93
|
+
.replace(/^'(.*)'$/, '"$1"')
|
|
94
|
+
.trim();
|
|
95
|
+
}
|
|
96
|
+
// Write stripped defs to a file that super can load with -I
|
|
97
|
+
const defsFile = join(tmpdir(), 'superkit-test-defs.spq');
|
|
98
|
+
writeFileSync(defsFile, allDefinitions);
|
|
99
|
+
function runSuper(query) {
|
|
100
|
+
const result = execFileSync('super', ['-I', defsFile, '-s', '-c', query], {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
timeout: 10_000,
|
|
103
|
+
});
|
|
104
|
+
return result.trim();
|
|
105
|
+
}
|
|
106
|
+
// Functions stripped from defs that can't be tested
|
|
107
|
+
const SKIPPED_FNS = new Set(UNPARSEABLE_FNS);
|
|
108
|
+
const { recipes } = superRecipes();
|
|
109
|
+
// Skip examples that are prose descriptions, not executable assertions
|
|
110
|
+
function isExecutableExample(example) {
|
|
111
|
+
const nonExecutable = [
|
|
112
|
+
'quoted and wrapped', 'single-quoted', 'tabs replaced',
|
|
113
|
+
'newlines replaced', 'safely embedded', 'properly escaped',
|
|
114
|
+
'single record', 'arbitrary user', 'formatted',
|
|
115
|
+
];
|
|
116
|
+
return !nonExecutable.some(s => example.o.toLowerCase().includes(s));
|
|
117
|
+
}
|
|
118
|
+
describe('recipe skdoc examples', () => {
|
|
119
|
+
for (const recipe of recipes) {
|
|
120
|
+
if (SKIPPED_FNS.has(recipe.name))
|
|
121
|
+
continue;
|
|
122
|
+
// Skip shell-pattern recipes (type: "shell") — not SuperDB functions
|
|
123
|
+
if (recipe.type === 'shell')
|
|
124
|
+
continue;
|
|
125
|
+
for (const example of recipe.examples) {
|
|
126
|
+
if (!isExecutableExample(example))
|
|
127
|
+
continue;
|
|
128
|
+
it(`${recipe.name}: ${example.i}`, () => {
|
|
129
|
+
// Some examples already include "values", don't double-prefix
|
|
130
|
+
const query = example.i.startsWith('values ')
|
|
131
|
+
? example.i
|
|
132
|
+
: `values ${example.i}`;
|
|
133
|
+
const actual = runSuper(query);
|
|
134
|
+
const expected = normalizeOutput(example.o);
|
|
135
|
+
// Strip type decorators (e.g., "29::uint8" → "29") for comparison
|
|
136
|
+
const actualClean = actual.replace(/::\w+$/, '');
|
|
137
|
+
expect(actualClean).toBe(expected);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=recipes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.test.js","sourceRoot":"","sources":["../src/recipes.test.ts"],"names":[],"mappings":"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"}
|
package/docs/recipes/escape.spq
CHANGED
|
@@ -21,7 +21,7 @@ fn skdoc_csv_row(): (
|
|
|
21
21
|
type:"func",
|
|
22
22
|
desc:"Builds a CSV row from an array of values. Each element is cast to string and escaped with sk_csv_field, then joined with commas.",
|
|
23
23
|
args:[{name:"arr",desc:"Array of values to format as a CSV row"}],
|
|
24
|
-
examples:[{i:"sk_csv_row(
|
|
24
|
+
examples:[{i:"sk_csv_row(['a', 'b,c', 'd'])",o:"'a,\"b,c\",d'"}] }, <skdoc>)
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
fn sk_csv_row(arr): (
|
package/docs/recipes/string.md
CHANGED
|
@@ -24,8 +24,8 @@ Returns a slice of the string passed in, even if indexes are out of range.
|
|
|
24
24
|
| `end` | Ending index, exclusive. |
|
|
25
25
|
|
|
26
26
|
```supersql
|
|
27
|
-
sk_slice('howdy')
|
|
28
|
-
-- => '
|
|
27
|
+
sk_slice('howdy', 0, 3)
|
|
28
|
+
-- => 'how'
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
**Implementation:**
|
|
@@ -235,15 +235,15 @@ super -I string.spq -s -c 'values sk_urldecode("%2Ftavern%20test")' -
|
|
|
235
235
|
op sk_decode_seg s: (
|
|
236
236
|
len(s) == 0
|
|
237
237
|
? s
|
|
238
|
-
: (is_error(hex(s[
|
|
238
|
+
: (is_error(hex(s[0:2]))
|
|
239
239
|
? s
|
|
240
|
-
: hex(s[
|
|
240
|
+
: f'{hex(s[0:2])::string}{s[2:]}')
|
|
241
241
|
)
|
|
242
242
|
|
|
243
243
|
op sk_urldecode url: (
|
|
244
|
-
split(url, "
|
|
244
|
+
split(url, "%%")
|
|
245
245
|
| unnest this
|
|
246
|
-
|
|
|
246
|
+
| sk_decode_seg this
|
|
247
247
|
| collect(this)
|
|
248
248
|
| join(this, "")
|
|
249
249
|
)
|
package/docs/recipes/string.spq
CHANGED
|
@@ -9,7 +9,7 @@ op skdoc_slice: (
|
|
|
9
9
|
args:[{name:"s",desc:"The string to slice."}
|
|
10
10
|
{name:"start",desc:"Starting index, zero-based, inclusive."}
|
|
11
11
|
{name:"end",desc:"Ending index, exclusive."}],
|
|
12
|
-
examples:[{i:"sk_slice('howdy')",o:"'
|
|
12
|
+
examples:[{i:"sk_slice('howdy', 0, 3)",o:"'how'"}] }, <skdoc>)
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
-- This isn't necessary with zq, but during early releases of super, the
|
|
@@ -114,21 +114,20 @@ fn skdoc_mid(): (
|
|
|
114
114
|
|
|
115
115
|
fn sk_mid(s, start, n): (sk_slice(s, sk_clamp(start, 0, len(s)), sk_clamp(start, 0, len(s)) + sk_clamp(n, 0, len(s))))
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-- Otherwise returns the segment as-is
|
|
117
|
+
fn skdoc_urldecode(): (
|
|
118
|
+
cast(
|
|
119
|
+
{name:"sk_urldecode",
|
|
120
|
+
type:"op",
|
|
121
|
+
desc:"URL-decodes a percent-encoded string. Splits on %%, decodes each hex-encoded segment, and joins back together.",
|
|
122
|
+
args:[{name:"url",desc:"The URL-encoded string to decode."}],
|
|
123
|
+
examples:[{i:"'%%2Fhello%%20world' | sk_urldecode this",o:"'/hello world'"}]}, <skdoc>)
|
|
124
|
+
)
|
|
126
125
|
op sk_decode_seg s: (
|
|
127
126
|
len(s) == 0
|
|
128
127
|
? s
|
|
129
|
-
: (is_error(hex(s[
|
|
128
|
+
: (is_error(hex(s[0:2]))
|
|
130
129
|
? s
|
|
131
|
-
: hex(s[
|
|
130
|
+
: f'{hex(s[0:2])::string}{s[2:]}')
|
|
132
131
|
)
|
|
133
132
|
|
|
134
133
|
-- Main URL decoder operator
|