@deterministic-code/datasources-cli 0.1.13 → 0.1.14
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/detect.js +21 -7
- package/dist/generate/CSharpRepositories.js +18 -14
- package/dist/generate/DeterministicProvider.js +23 -7
- package/dist/generate/RepositoryModel.js +10 -4
- package/dist/generate/RepositoryPlan.js +12 -5
- package/dist/generate/RustRepositories.js +42 -31
- package/dist/generate/TypeScriptRepositories.js +22 -18
- package/dist/generate/applyGenerate.js +9 -2
- package/dist/generate/applyMigrations.js +5 -13
- package/dist/generate/casing.js +28 -0
- package/dist/generate/migrateUp.js +2 -8
- package/dist/generate/sqlFlags.js +27 -0
- package/dist/migrateInit.js +4 -3
- package/dist/parse.js +102 -25
- package/dist/run.js +42 -67
- package/dist/setup.js +7 -9
- package/dist/version/applyVersion.js +11 -14
- package/dist/version/delta.js +0 -50
- package/dist/version/render.js +1 -11
- package/dist/wireRegister.js +29 -11
- package/dist/wireRustProviders.js +42 -0
- package/node_modules/@deterministic-code/csharp-generators/dist/import-generator.js +16 -12
- package/node_modules/@deterministic-code/csharp-generators/src/import-generator.test.ts +22 -11
- package/node_modules/@deterministic-code/csharp-generators/src/import-generator.ts +21 -13
- package/node_modules/@deterministic-code/datasources-code-generators/dist/nestedSelectTypes.js +2 -6
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.d.ts +2 -0
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.js +37 -0
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.d.ts +2 -0
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.js +6 -0
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.d.ts +1 -1
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.js +3 -6
- package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/version-migrations.js +25 -14
- package/node_modules/@deterministic-code/datasources-code-generators/package.json +4 -2
- package/node_modules/@deterministic-code/deterministic-generators-common/README.md +61 -1
- package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/dist/specification-parser.js +128 -1
- package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/specification-parser.ts +1 -0
- package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/version-snapshot.ts +154 -0
- package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/yaml-node.ts +2 -2
- package/node_modules/@deterministic-code/rust-generators/dist/import-generator.js +25 -15
- package/node_modules/@deterministic-code/rust-generators/src/import-generator.test.ts +11 -0
- package/node_modules/@deterministic-code/rust-generators/src/import-generator.ts +36 -18
- package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-types.js +6 -1
- package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-validators.js +6 -1
- package/node_modules/@deterministic-code/typescript-generators/dist/generate-routes.js +1 -1
- package/node_modules/@deterministic-code/typescript-generators/dist/generate-services.js +1 -1
- package/node_modules/@deterministic-code/typescript-generators/dist/import-generator.js +29 -21
- package/node_modules/@deterministic-code/typescript-generators/src/emit.ts +2 -5
- package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-types.ts +7 -1
- package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-validators.ts +7 -1
- package/node_modules/@deterministic-code/typescript-generators/src/generate-routes.ts +1 -1
- package/node_modules/@deterministic-code/typescript-generators/src/generate-services.ts +1 -1
- package/node_modules/@deterministic-code/typescript-generators/src/import-generator.test.ts +16 -0
- package/node_modules/@deterministic-code/typescript-generators/src/import-generator.ts +37 -22
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Parser and validators for the [deterministic-specifications](https://github.com/deterministic-code/deterministic-specifications) contract. Each language is its own library. Specs and samples stay in the specifications repo; these libraries only read them.
|
|
4
4
|
|
|
5
|
-
- [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` on the `./parser` export, validators on the package root)
|
|
5
|
+
- [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` and `VersionSnapshot` on the `./parser` export, validators on the package root)
|
|
6
6
|
- [`rust/`](rust) — crate `deterministic_generators_common` (path/git dependency)
|
|
7
7
|
- [`csharp/`](csharp) — `Deterministic.Generators.Common` (`net10.0` class library)
|
|
8
8
|
|
|
@@ -15,8 +15,68 @@ deterministic_generators_common = { git = "https://github.com/deterministic-code
|
|
|
15
15
|
<ProjectReference Include="path/to/deterministic-generators-common/csharp/Deterministic.Generators.Common/Deterministic.Generators.Common.csproj" />
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
```xml
|
|
19
|
+
<PackageReference Include="Deterministic.Generators.Common" Version="0.1.1" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
TypeScript packs pin `#main`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
"@deterministic-code/deterministic-generators-common": "github:deterministic-code/deterministic-generators-common#main"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
C# first-party consumers use the `ProjectReference`. Out-of-repo C# can take the NuGet package.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
npm run publish:nuget # pack and smoke-test
|
|
32
|
+
npm run publish:nuget -- --publish # push to nuget.org (NUGET_API_KEY)
|
|
33
|
+
```
|
|
34
|
+
|
|
18
35
|
```text
|
|
19
36
|
npm test # TypeScript parser + validator (100% coverage)
|
|
20
37
|
sh rust/coverage.sh # cargo llvm-cov, 100% lines + functions
|
|
21
38
|
dotnet test csharp/Deterministic.Generators.Common.slnx --settings csharp/coverlet.runsettings
|
|
22
39
|
```
|
|
40
|
+
|
|
41
|
+
## VersionSnapshot
|
|
42
|
+
|
|
43
|
+
`VersionSnapshot` reconstructs specification YAML by folding every `deterministic/version-*/diff.yaml` in name order. Documents are the library YAML AST (`YamlValue` / `YamlMap` / `YamlSeq` / `YamlScalar`), not compiled `IDeterministic` and not `DeterministicParser` output.
|
|
44
|
+
|
|
45
|
+
| API | Role |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `Combine` / `combine` | Fold already-parsed diff trees (mappings), in order |
|
|
48
|
+
| `Read` / `read` | Scan a `deterministic/` directory and fold matching version dirs |
|
|
49
|
+
| `Documents` / `documents` | Reconstructed file path → tree |
|
|
50
|
+
| `Yaml` / `yaml(path)` | Serialize one reconstructed file with the library serializer |
|
|
51
|
+
|
|
52
|
+
Apply helpers are private instance methods. Version directories must match `^version-\d{8}T\d{6}Z-\d{3}$`. Missing or empty `diff.yaml` is skipped. A non-mapping diff throws `expected a mapping`. TypeScript `read` is async; C# and Rust `Read` are sync.
|
|
53
|
+
|
|
54
|
+
```csharp
|
|
55
|
+
var snapshot = VersionSnapshot.Read(deterministicRoot);
|
|
56
|
+
var types = snapshot.Yaml("types.yaml");
|
|
57
|
+
var datasource = snapshot.Yaml("datasource.yaml");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
|
|
62
|
+
|
|
63
|
+
const snapshot = await VersionSnapshot.read(deterministicRoot);
|
|
64
|
+
const types = snapshot.yaml("types.yaml");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```rust
|
|
68
|
+
let snapshot = VersionSnapshot::read(deterministic_root)?;
|
|
69
|
+
let types = snapshot.yaml("types.yaml")?;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`Combine` takes an ordered list of parsed mappings, not YAML strings. To compare a fold to a live file, wrap the live tree as `{ added: true, content }` and compare `Yaml` strings from the same serializer — do not string-compare authored YAML (comments and flow sequences differ).
|
|
73
|
+
|
|
74
|
+
Per file key in a diff:
|
|
75
|
+
|
|
76
|
+
- `{ added: true, content }` inserts `content` (`true` or the strings `"true"` / `"True"`).
|
|
77
|
+
- `{ removed: true }` deletes the file.
|
|
78
|
+
- Object merge: nested apply, `removed:` string list deletes keys, new keys are appended.
|
|
79
|
+
- Keyed collections (sequence of single-key maps): `{ added, changed, removed }`. Added entries are prepended.
|
|
80
|
+
- Otherwise the next value replaces.
|
|
81
|
+
|
|
82
|
+
`Read` does not grow `IDeterministicReader`.
|
|
@@ -1355,6 +1355,133 @@ var Parser = class {
|
|
|
1355
1355
|
return nested;
|
|
1356
1356
|
}
|
|
1357
1357
|
};
|
|
1358
|
+
|
|
1359
|
+
// node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/version-snapshot.ts
|
|
1360
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1361
|
+
import { join } from "node:path";
|
|
1362
|
+
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
1363
|
+
var versionDir = /^version-\d{8}T\d{6}Z-\d{3}$/;
|
|
1364
|
+
var own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
1365
|
+
var VersionSnapshot = class _VersionSnapshot {
|
|
1366
|
+
documents;
|
|
1367
|
+
constructor(diffsInOrder) {
|
|
1368
|
+
const docs = {};
|
|
1369
|
+
for (const diff of diffsInOrder) {
|
|
1370
|
+
if (diff == null) continue;
|
|
1371
|
+
if (!this.isMap(diff)) throw new Error("expected a mapping");
|
|
1372
|
+
for (const [path, envelope] of Object.entries(diff)) {
|
|
1373
|
+
const next = this.applyFile(docs[path], envelope);
|
|
1374
|
+
if (this.isNull(next)) delete docs[path];
|
|
1375
|
+
else docs[path] = next;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
this.documents = docs;
|
|
1379
|
+
}
|
|
1380
|
+
static combine(diffsInOrder) {
|
|
1381
|
+
return new _VersionSnapshot(diffsInOrder);
|
|
1382
|
+
}
|
|
1383
|
+
static async read(deterministicRoot) {
|
|
1384
|
+
const names = (await readdir(deterministicRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory() && versionDir.test(entry.name)).map((entry) => entry.name).sort();
|
|
1385
|
+
const snapshot = new _VersionSnapshot([]);
|
|
1386
|
+
return _VersionSnapshot.combine(await Promise.all(names.map((name) => snapshot.diffOf(deterministicRoot, name))));
|
|
1387
|
+
}
|
|
1388
|
+
yaml(path) {
|
|
1389
|
+
if (!own(this.documents, path)) throw new Error(`missing ${path}`);
|
|
1390
|
+
return stringify2(this.documents[path]);
|
|
1391
|
+
}
|
|
1392
|
+
async diffOf(root, name) {
|
|
1393
|
+
let text;
|
|
1394
|
+
try {
|
|
1395
|
+
text = await readFile(join(root, name, "diff.yaml"), "utf8");
|
|
1396
|
+
} catch {
|
|
1397
|
+
return void 0;
|
|
1398
|
+
}
|
|
1399
|
+
if (text === "") return void 0;
|
|
1400
|
+
let rootValue;
|
|
1401
|
+
try {
|
|
1402
|
+
rootValue = parse2(text);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
throw new Error(`${name}/diff.yaml: ${String(error)}`);
|
|
1405
|
+
}
|
|
1406
|
+
if (!this.isMap(rootValue)) throw new Error(`${name}/diff.yaml: expected a mapping`);
|
|
1407
|
+
return rootValue;
|
|
1408
|
+
}
|
|
1409
|
+
applyFile(before, envelope) {
|
|
1410
|
+
if (this.isNull(envelope)) return before;
|
|
1411
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "removed") && Object.keys(envelope).length === 1)
|
|
1412
|
+
return void 0;
|
|
1413
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "added")) return envelope.content;
|
|
1414
|
+
return this.apply(before, envelope);
|
|
1415
|
+
}
|
|
1416
|
+
apply(before, next) {
|
|
1417
|
+
if (this.isNull(next)) return before;
|
|
1418
|
+
if (this.isKeyed(before) && this.isMap(next) && this.isSplit(next)) return this.applyKeyed(before, next);
|
|
1419
|
+
if (this.isMap(before) && this.isMap(next)) return this.applyObject(before, next);
|
|
1420
|
+
return next;
|
|
1421
|
+
}
|
|
1422
|
+
applyKeyed(beforeVal, split) {
|
|
1423
|
+
const removed = new Set(this.strings(split, "removed"));
|
|
1424
|
+
const changed = new Map(
|
|
1425
|
+
this.seq(split.changed).flatMap((entry) => {
|
|
1426
|
+
const pair = this.firstPair(entry);
|
|
1427
|
+
return pair ? [[pair.key, pair.value]] : [];
|
|
1428
|
+
})
|
|
1429
|
+
);
|
|
1430
|
+
return [
|
|
1431
|
+
...this.seq(split.added),
|
|
1432
|
+
...beforeVal.flatMap((entry) => {
|
|
1433
|
+
const pair = this.firstPair(entry);
|
|
1434
|
+
if (!pair || removed.has(pair.key)) return [];
|
|
1435
|
+
return [
|
|
1436
|
+
changed.has(pair.key) ? { [pair.key]: this.apply(pair.value, changed.get(pair.key)) } : entry
|
|
1437
|
+
];
|
|
1438
|
+
})
|
|
1439
|
+
];
|
|
1440
|
+
}
|
|
1441
|
+
applyObject(beforeVal, next) {
|
|
1442
|
+
const removed = new Set(this.strings(next, "removed"));
|
|
1443
|
+
const output = {};
|
|
1444
|
+
for (const [key, value] of Object.entries(beforeVal)) {
|
|
1445
|
+
if (removed.has(key)) continue;
|
|
1446
|
+
output[key] = own(next, key) ? this.apply(value, next[key]) : value;
|
|
1447
|
+
}
|
|
1448
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1449
|
+
if (key === "removed" && Array.isArray(value)) continue;
|
|
1450
|
+
if (!own(beforeVal, key)) output[key] = value;
|
|
1451
|
+
}
|
|
1452
|
+
return output;
|
|
1453
|
+
}
|
|
1454
|
+
isSplit(map) {
|
|
1455
|
+
return Object.keys(map).every((key) => key === "added" || key === "changed" || key === "removed");
|
|
1456
|
+
}
|
|
1457
|
+
isKeyed(value) {
|
|
1458
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => this.isMap(item) && Object.keys(item).length === 1);
|
|
1459
|
+
}
|
|
1460
|
+
seq(value) {
|
|
1461
|
+
return Array.isArray(value) ? value : [];
|
|
1462
|
+
}
|
|
1463
|
+
strings(map, key) {
|
|
1464
|
+
return this.seq(map[key]).map((item) => this.isScalar(item) ? String(item ?? "") : "");
|
|
1465
|
+
}
|
|
1466
|
+
firstPair(entry) {
|
|
1467
|
+
if (!this.isMap(entry)) return void 0;
|
|
1468
|
+
const key = Object.keys(entry)[0];
|
|
1469
|
+
return key === void 0 ? void 0 : { key, value: entry[key] };
|
|
1470
|
+
}
|
|
1471
|
+
isMap(value) {
|
|
1472
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1473
|
+
}
|
|
1474
|
+
isTrue(map, key) {
|
|
1475
|
+
return map[key] === true || map[key] === "true" || map[key] === "True";
|
|
1476
|
+
}
|
|
1477
|
+
isNull(value) {
|
|
1478
|
+
return value == null;
|
|
1479
|
+
}
|
|
1480
|
+
isScalar(value) {
|
|
1481
|
+
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1358
1484
|
export {
|
|
1359
|
-
DeterministicParser
|
|
1485
|
+
DeterministicParser,
|
|
1486
|
+
VersionSnapshot
|
|
1360
1487
|
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import type { YamlMap, YamlValue } from "./yaml-node.ts";
|
|
5
|
+
|
|
6
|
+
const versionDir = /^version-\d{8}T\d{6}Z-\d{3}$/;
|
|
7
|
+
const own = (obj: object, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
8
|
+
|
|
9
|
+
export class VersionSnapshot {
|
|
10
|
+
readonly documents: Readonly<Record<string, YamlValue | undefined>>;
|
|
11
|
+
|
|
12
|
+
private constructor(diffsInOrder: Array<YamlValue | null | undefined>) {
|
|
13
|
+
const docs: Record<string, YamlValue | undefined> = {};
|
|
14
|
+
for (const diff of diffsInOrder) {
|
|
15
|
+
if (diff == null) continue;
|
|
16
|
+
if (!this.isMap(diff)) throw new Error("expected a mapping");
|
|
17
|
+
for (const [path, envelope] of Object.entries(diff)) {
|
|
18
|
+
const next = this.applyFile(docs[path], envelope);
|
|
19
|
+
if (this.isNull(next)) delete docs[path];
|
|
20
|
+
else docs[path] = next;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
this.documents = docs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static combine(diffsInOrder: Array<YamlValue | null | undefined>): VersionSnapshot {
|
|
27
|
+
return new VersionSnapshot(diffsInOrder);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async read(deterministicRoot: string): Promise<VersionSnapshot> {
|
|
31
|
+
const names = (await readdir(deterministicRoot, { withFileTypes: true }))
|
|
32
|
+
.filter((entry) => entry.isDirectory() && versionDir.test(entry.name))
|
|
33
|
+
.map((entry) => entry.name)
|
|
34
|
+
.sort();
|
|
35
|
+
const snapshot = new VersionSnapshot([]);
|
|
36
|
+
return VersionSnapshot.combine(await Promise.all(names.map((name) => snapshot.diffOf(deterministicRoot, name))));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yaml(path: string): string {
|
|
40
|
+
if (!own(this.documents, path)) throw new Error(`missing ${path}`);
|
|
41
|
+
return stringify(this.documents[path]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async diffOf(root: string, name: string): Promise<YamlValue | undefined> {
|
|
45
|
+
let text: string;
|
|
46
|
+
try {
|
|
47
|
+
text = await readFile(join(root, name, "diff.yaml"), "utf8");
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (text === "") return undefined;
|
|
52
|
+
let rootValue: YamlValue;
|
|
53
|
+
try {
|
|
54
|
+
rootValue = parse(text) as YamlValue;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`${name}/diff.yaml: ${String(error)}`);
|
|
57
|
+
}
|
|
58
|
+
if (!this.isMap(rootValue)) throw new Error(`${name}/diff.yaml: expected a mapping`);
|
|
59
|
+
return rootValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private applyFile(before: YamlValue | undefined, envelope: YamlValue | undefined): YamlValue | undefined {
|
|
63
|
+
if (this.isNull(envelope)) return before;
|
|
64
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "removed") && Object.keys(envelope).length === 1)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "added")) return envelope.content;
|
|
67
|
+
return this.apply(before, envelope);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private apply(before: YamlValue | undefined, next: YamlValue | undefined): YamlValue | undefined {
|
|
71
|
+
if (this.isNull(next)) return before;
|
|
72
|
+
if (this.isKeyed(before) && this.isMap(next) && this.isSplit(next)) return this.applyKeyed(before, next);
|
|
73
|
+
if (this.isMap(before) && this.isMap(next)) return this.applyObject(before, next);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private applyKeyed(beforeVal: YamlMap[], split: YamlMap): YamlValue {
|
|
78
|
+
const removed = new Set(this.strings(split, "removed"));
|
|
79
|
+
const changed = new Map(
|
|
80
|
+
this.seq(split.changed).flatMap((entry) => {
|
|
81
|
+
const pair = this.firstPair(entry);
|
|
82
|
+
return pair ? [[pair.key, pair.value] as const] : [];
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
return [
|
|
86
|
+
...this.seq(split.added),
|
|
87
|
+
...beforeVal.flatMap((entry) => {
|
|
88
|
+
const pair = this.firstPair(entry);
|
|
89
|
+
if (!pair || removed.has(pair.key)) return [];
|
|
90
|
+
return [
|
|
91
|
+
changed.has(pair.key)
|
|
92
|
+
? { [pair.key]: this.apply(pair.value, changed.get(pair.key)) as YamlValue }
|
|
93
|
+
: entry,
|
|
94
|
+
];
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private applyObject(beforeVal: YamlMap, next: YamlMap): YamlMap {
|
|
100
|
+
const removed = new Set(this.strings(next, "removed"));
|
|
101
|
+
const output: Record<string, YamlValue> = {};
|
|
102
|
+
for (const [key, value] of Object.entries(beforeVal)) {
|
|
103
|
+
if (removed.has(key)) continue;
|
|
104
|
+
output[key] = (own(next, key) ? this.apply(value, next[key]) : value) as YamlValue;
|
|
105
|
+
}
|
|
106
|
+
for (const [key, value] of Object.entries(next)) {
|
|
107
|
+
if (key === "removed" && Array.isArray(value)) continue;
|
|
108
|
+
if (!own(beforeVal, key)) output[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private isSplit(map: YamlMap) {
|
|
114
|
+
return Object.keys(map).every((key) => key === "added" || key === "changed" || key === "removed");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private isKeyed(value: YamlValue | undefined): value is YamlMap[] {
|
|
118
|
+
return (
|
|
119
|
+
Array.isArray(value) &&
|
|
120
|
+
value.length > 0 &&
|
|
121
|
+
value.every((item) => this.isMap(item) && Object.keys(item).length === 1)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private seq(value: YamlValue | undefined): YamlValue[] {
|
|
126
|
+
return Array.isArray(value) ? value : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private strings(map: YamlMap, key: string) {
|
|
130
|
+
return this.seq(map[key]).map((item) => (this.isScalar(item) ? String(item ?? "") : ""));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private firstPair(entry: YamlValue | undefined) {
|
|
134
|
+
if (!this.isMap(entry)) return undefined;
|
|
135
|
+
const key = Object.keys(entry)[0];
|
|
136
|
+
return key === undefined ? undefined : { key, value: entry[key] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private isMap(value: YamlValue | undefined | null): value is YamlMap {
|
|
140
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private isTrue(map: YamlMap, key: string) {
|
|
144
|
+
return map[key] === true || map[key] === "true" || map[key] === "True";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private isNull(value: YamlValue | undefined | null) {
|
|
148
|
+
return value == null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private isScalar(value: YamlValue | undefined) {
|
|
152
|
+
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parse } from "yaml";
|
|
2
2
|
|
|
3
3
|
type YamlLiteral = string | number | boolean | null;
|
|
4
|
-
type YamlMap = { readonly [key: string]: YamlValue };
|
|
5
|
-
type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
|
|
4
|
+
export type YamlMap = { readonly [key: string]: YamlValue };
|
|
5
|
+
export type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
|
|
6
6
|
type FieldDefault = YamlLiteral | YamlValue[] | YamlMap;
|
|
7
7
|
type Named = { name: string; node: YamlNode };
|
|
8
8
|
|
|
@@ -45,7 +45,7 @@ class RustImportGenerator {
|
|
|
45
45
|
return this.underBase(path);
|
|
46
46
|
}
|
|
47
47
|
datasourceRel(entity) {
|
|
48
|
-
return this.rel("types/generated/datasource", this.datasource(entity));
|
|
48
|
+
return this.rel(this.layered("types/generated/datasource"), this.datasource(entity));
|
|
49
49
|
}
|
|
50
50
|
datasourceQual(entity) {
|
|
51
51
|
return this.typeQual(
|
|
@@ -63,7 +63,7 @@ class RustImportGenerator {
|
|
|
63
63
|
}
|
|
64
64
|
datasourceValidatorRel(entity) {
|
|
65
65
|
return this.rel(
|
|
66
|
-
"types/generated/datasource/validators",
|
|
66
|
+
this.layered("types/generated/datasource/validators"),
|
|
67
67
|
this.datasourceValidator(entity)
|
|
68
68
|
);
|
|
69
69
|
}
|
|
@@ -73,7 +73,7 @@ class RustImportGenerator {
|
|
|
73
73
|
return this.underBase(path);
|
|
74
74
|
}
|
|
75
75
|
viewRel(entity) {
|
|
76
|
-
return this.rel("types/generated/views", this.view(entity));
|
|
76
|
+
return this.rel(this.layered("types/generated/views"), this.view(entity));
|
|
77
77
|
}
|
|
78
78
|
viewQual(entity) {
|
|
79
79
|
return this.typeQual(this.viewRel(entity), this.casing.convertTypes(entity));
|
|
@@ -87,7 +87,7 @@ class RustImportGenerator {
|
|
|
87
87
|
}
|
|
88
88
|
viewValidatorRel(entity) {
|
|
89
89
|
return this.rel(
|
|
90
|
-
"types/generated/views/validators",
|
|
90
|
+
this.layered("types/generated/views/validators"),
|
|
91
91
|
this.viewValidator(entity)
|
|
92
92
|
);
|
|
93
93
|
}
|
|
@@ -97,31 +97,31 @@ class RustImportGenerator {
|
|
|
97
97
|
return this.underBase(path);
|
|
98
98
|
}
|
|
99
99
|
serviceRel(entity) {
|
|
100
|
-
return this.rel("services/generated", this.service(entity));
|
|
100
|
+
return this.rel(this.layered("services/generated"), this.service(entity));
|
|
101
101
|
}
|
|
102
102
|
serviceCustom(name, module) {
|
|
103
103
|
return this.resolveCustom(name, module, "services");
|
|
104
104
|
}
|
|
105
105
|
serviceCustomRel(entity) {
|
|
106
106
|
const file = `${this.serviceStem(entity)}.rs`;
|
|
107
|
-
return this.organizeByFeature ?
|
|
107
|
+
return this.organizeByFeature ? `${this.featuresDir(entity)}/custom/${file}` : `${this.layered("services")}/custom/${file}`;
|
|
108
108
|
}
|
|
109
109
|
serviceTest(entity) {
|
|
110
110
|
return this.test(this.service(entity), `${entity}_service`);
|
|
111
111
|
}
|
|
112
112
|
serviceTestRel(entity) {
|
|
113
|
-
return this.rel("services/generated/__tests__", this.serviceTest(entity));
|
|
113
|
+
return this.rel(this.layered("services/generated/__tests__"), this.serviceTest(entity));
|
|
114
114
|
}
|
|
115
115
|
serviceIntegrationTest(entity) {
|
|
116
116
|
const file = this.rsFile(`${entity}_service_integration_tests`);
|
|
117
117
|
if (this.organizeByFeature) {
|
|
118
|
-
return
|
|
118
|
+
return `${this.featuresDir(entity)}/__tests__/${file}`;
|
|
119
119
|
}
|
|
120
120
|
return file;
|
|
121
121
|
}
|
|
122
122
|
serviceIntegrationTestRel(entity) {
|
|
123
123
|
return this.rel(
|
|
124
|
-
"services/generated/__tests__",
|
|
124
|
+
this.layered("services/generated/__tests__"),
|
|
125
125
|
this.serviceIntegrationTest(entity)
|
|
126
126
|
);
|
|
127
127
|
}
|
|
@@ -134,7 +134,7 @@ class RustImportGenerator {
|
|
|
134
134
|
return this.underBase(path);
|
|
135
135
|
}
|
|
136
136
|
routeRel(entity) {
|
|
137
|
-
return this.rel("routes/generated", this.route(entity));
|
|
137
|
+
return this.rel(this.layered("routes/generated"), this.route(entity));
|
|
138
138
|
}
|
|
139
139
|
routeCustom(name, module) {
|
|
140
140
|
return this.resolveCustom(name, module, "routes");
|
|
@@ -166,15 +166,17 @@ class RustImportGenerator {
|
|
|
166
166
|
return this.routeStem(entity);
|
|
167
167
|
}
|
|
168
168
|
appWiring() {
|
|
169
|
-
return this.organizeByFeature ? "features/app_wiring.rs
|
|
169
|
+
return this.organizeByFeature ? `${this.casing.convertDirectories("features")}/app_wiring.rs` : "app_wiring.rs";
|
|
170
170
|
}
|
|
171
171
|
validatorFn(kind, entity, fn) {
|
|
172
172
|
if (this.organizeByFeature) {
|
|
173
173
|
const file = kind === "datasource" ? this.datasourceValidator(entity) : this.viewValidator(entity);
|
|
174
174
|
return `${this.spec("", file)}::${fn}`;
|
|
175
175
|
}
|
|
176
|
-
const ns =
|
|
177
|
-
|
|
176
|
+
const ns = this.layered(
|
|
177
|
+
kind === "datasource" ? "types/generated/datasource/validators" : "types/generated/views/validators"
|
|
178
|
+
).replaceAll("/", "::");
|
|
179
|
+
return `crate::${ns}::${fn}`;
|
|
178
180
|
}
|
|
179
181
|
apiPath(entity) {
|
|
180
182
|
return pluralSnake(entity).replace(/_/g, "-");
|
|
@@ -196,8 +198,16 @@ class RustImportGenerator {
|
|
|
196
198
|
rsFile(stem) {
|
|
197
199
|
return `${this.casing.fileBase(stem)}.rs`;
|
|
198
200
|
}
|
|
201
|
+
layered(path) {
|
|
202
|
+
return path.split("/").map(
|
|
203
|
+
(segment) => segment === "custom" || segment === "__tests__" ? segment : this.casing.convertDirectories(segment)
|
|
204
|
+
).join("/");
|
|
205
|
+
}
|
|
206
|
+
featuresDir(entity) {
|
|
207
|
+
return `${this.casing.convertDirectories("features")}/${this.casing.directory(entity)}`;
|
|
208
|
+
}
|
|
199
209
|
featurePath(entity, file) {
|
|
200
|
-
return
|
|
210
|
+
return `${this.featuresDir(entity)}/${file}`;
|
|
201
211
|
}
|
|
202
212
|
serviceStem(entity) {
|
|
203
213
|
return this.casing.fileBase(`${entity}_service`);
|
|
@@ -217,7 +227,7 @@ class RustImportGenerator {
|
|
|
217
227
|
const stubFn = layer === "services" ? "generateCustomServiceStub" : "generateCustomRouteStub";
|
|
218
228
|
const file = this.rsFile(name);
|
|
219
229
|
const entity = (featureEntityFromClass(name) || "shared").replace(/-/g, "_");
|
|
220
|
-
const defaultStub = this.organizeByFeature ?
|
|
230
|
+
const defaultStub = this.organizeByFeature ? `${this.featuresDir(entity)}/custom/${file}` : `../custom/${file}`;
|
|
221
231
|
if (this.organizeByFeature) {
|
|
222
232
|
if (!mod || !mod.startsWith(".") || mod.startsWith("./services/") || mod.startsWith("./routes/")) {
|
|
223
233
|
return defaultStub;
|
|
@@ -95,6 +95,17 @@ describe("RustImportGenerator layered (organize_by_feature unset)", () => {
|
|
|
95
95
|
imports.validatorFn("view", "user", "validate_user"),
|
|
96
96
|
"crate::types::generated::views::validators::validate_user",
|
|
97
97
|
);
|
|
98
|
+
const pascalDirs = createImportGenerator(".", {
|
|
99
|
+
"languages.rust.casing.directories": "Pascal",
|
|
100
|
+
});
|
|
101
|
+
assert.equal(
|
|
102
|
+
pascalDirs.datasourceRel("user"),
|
|
103
|
+
"Types/Generated/Datasource/user.rs",
|
|
104
|
+
);
|
|
105
|
+
assert.equal(
|
|
106
|
+
pascalDirs.validatorFn("datasource", "user", "validate_datasource_user"),
|
|
107
|
+
"crate::Types::Generated::Datasource::Validators::validate_datasource_user",
|
|
108
|
+
);
|
|
98
109
|
assert.equal(imports.apiPath("card_payment"), "card-payments");
|
|
99
110
|
assert.equal(imports.apiPath("user"), "users");
|
|
100
111
|
assert.equal(imports.frontend("src/main.rs"), "frontend/src/main.rs");
|
|
@@ -67,7 +67,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
datasourceRel(entity: string): string {
|
|
70
|
-
return this.rel("types/generated/datasource", this.datasource(entity));
|
|
70
|
+
return this.rel(this.layered("types/generated/datasource"), this.datasource(entity));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
datasourceQual(entity: string): string {
|
|
@@ -88,7 +88,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
88
88
|
|
|
89
89
|
datasourceValidatorRel(entity: string): string {
|
|
90
90
|
return this.rel(
|
|
91
|
-
"types/generated/datasource/validators",
|
|
91
|
+
this.layered("types/generated/datasource/validators"),
|
|
92
92
|
this.datasourceValidator(entity),
|
|
93
93
|
);
|
|
94
94
|
}
|
|
@@ -102,7 +102,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
viewRel(entity: string): string {
|
|
105
|
-
return this.rel("types/generated/views", this.view(entity));
|
|
105
|
+
return this.rel(this.layered("types/generated/views"), this.view(entity));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
viewQual(entity: string): string {
|
|
@@ -119,7 +119,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
119
119
|
|
|
120
120
|
viewValidatorRel(entity: string): string {
|
|
121
121
|
return this.rel(
|
|
122
|
-
"types/generated/views/validators",
|
|
122
|
+
this.layered("types/generated/views/validators"),
|
|
123
123
|
this.viewValidator(entity),
|
|
124
124
|
);
|
|
125
125
|
}
|
|
@@ -133,7 +133,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
serviceRel(entity: string): string {
|
|
136
|
-
return this.rel("services/generated", this.service(entity));
|
|
136
|
+
return this.rel(this.layered("services/generated"), this.service(entity));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
serviceCustom(name: string, module?: string): string {
|
|
@@ -143,8 +143,8 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
143
143
|
serviceCustomRel(entity: string): string {
|
|
144
144
|
const file = `${this.serviceStem(entity)}.rs`;
|
|
145
145
|
return this.organizeByFeature
|
|
146
|
-
?
|
|
147
|
-
:
|
|
146
|
+
? `${this.featuresDir(entity)}/custom/${file}`
|
|
147
|
+
: `${this.layered("services")}/custom/${file}`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
serviceTest(entity: string): string {
|
|
@@ -152,20 +152,20 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
serviceTestRel(entity: string): string {
|
|
155
|
-
return this.rel("services/generated/__tests__", this.serviceTest(entity));
|
|
155
|
+
return this.rel(this.layered("services/generated/__tests__"), this.serviceTest(entity));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
serviceIntegrationTest(entity: string): string {
|
|
159
159
|
const file = this.rsFile(`${entity}_service_integration_tests`);
|
|
160
160
|
if (this.organizeByFeature) {
|
|
161
|
-
return
|
|
161
|
+
return `${this.featuresDir(entity)}/__tests__/${file}`;
|
|
162
162
|
}
|
|
163
163
|
return file;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
serviceIntegrationTestRel(entity: string): string {
|
|
167
167
|
return this.rel(
|
|
168
|
-
"services/generated/__tests__",
|
|
168
|
+
this.layered("services/generated/__tests__"),
|
|
169
169
|
this.serviceIntegrationTest(entity),
|
|
170
170
|
);
|
|
171
171
|
}
|
|
@@ -183,7 +183,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
routeRel(entity: string): string {
|
|
186
|
-
return this.rel("routes/generated", this.route(entity));
|
|
186
|
+
return this.rel(this.layered("routes/generated"), this.route(entity));
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
routeCustom(name: string, module?: string): string {
|
|
@@ -224,7 +224,9 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
appWiring(): string {
|
|
227
|
-
return this.organizeByFeature
|
|
227
|
+
return this.organizeByFeature
|
|
228
|
+
? `${this.casing.convertDirectories("features")}/app_wiring.rs`
|
|
229
|
+
: "app_wiring.rs";
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
validatorFn(
|
|
@@ -239,11 +241,12 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
239
241
|
: this.viewValidator(entity);
|
|
240
242
|
return `${this.spec("", file)}::${fn}`;
|
|
241
243
|
}
|
|
242
|
-
const ns =
|
|
244
|
+
const ns = this.layered(
|
|
243
245
|
kind === "datasource"
|
|
244
|
-
? "
|
|
245
|
-
: "
|
|
246
|
-
|
|
246
|
+
? "types/generated/datasource/validators"
|
|
247
|
+
: "types/generated/views/validators",
|
|
248
|
+
).replaceAll("/", "::");
|
|
249
|
+
return `crate::${ns}::${fn}`;
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
apiPath(entity: string): string {
|
|
@@ -271,8 +274,23 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
271
274
|
return `${this.casing.fileBase(stem)}.rs`;
|
|
272
275
|
}
|
|
273
276
|
|
|
277
|
+
private layered(path: string): string {
|
|
278
|
+
return path
|
|
279
|
+
.split("/")
|
|
280
|
+
.map((segment) =>
|
|
281
|
+
segment === "custom" || segment === "__tests__"
|
|
282
|
+
? segment
|
|
283
|
+
: this.casing.convertDirectories(segment),
|
|
284
|
+
)
|
|
285
|
+
.join("/");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private featuresDir(entity: string): string {
|
|
289
|
+
return `${this.casing.convertDirectories("features")}/${this.casing.directory(entity)}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
private featurePath(entity: string, file: string): string {
|
|
275
|
-
return
|
|
293
|
+
return `${this.featuresDir(entity)}/${file}`;
|
|
276
294
|
}
|
|
277
295
|
|
|
278
296
|
private serviceStem(entity: string): string {
|
|
@@ -306,7 +324,7 @@ export class RustImportGenerator implements IImportGenerator {
|
|
|
306
324
|
featureEntityFromClass(name) || "shared"
|
|
307
325
|
).replace(/-/g, "_");
|
|
308
326
|
const defaultStub = this.organizeByFeature
|
|
309
|
-
?
|
|
327
|
+
? `${this.featuresDir(entity)}/custom/${file}`
|
|
310
328
|
: `../custom/${file}`;
|
|
311
329
|
if (this.organizeByFeature) {
|
|
312
330
|
if (
|