Skip to content

Commit 95f2fef

Browse files
authored
Extract generic test ref parser (#45)
1 parent 0aac4c0 commit 95f2fef

3 files changed

Lines changed: 51 additions & 19 deletions

File tree

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ Exit criteria:
569569

570570
- M0 is complete as the Zig `0.16.0` runtime scaffold.
571571
- M1 has started on stacked branches with native Zig artifact-first parser slices.
572-
- M1A has started with behavior-preserving `src/project/types.zig`, `src/project/selector.zig`, `src/project/manifest.zig`, `src/project/util.zig`, `src/project/config.zig`, `src/project/fs.zig`, `src/project/jinja.zig`, `src/project/resolve.zig`, and `src/project/parse.zig` extractions. `src/project/manifest.zig` owns selected-resource JSON and partial `manifest.json` writing with native tests for selected JSON shape, JSON escaping, exposure dependency ordering, disabled-resource filtering, and graph-map output. `src/project/util.zig` owns shared display, membership, append-dedup, string sorting, and narrow YAML scalar/list helpers. `src/project/config.zig` owns `dbt_project.yml` loading, project path/docs config parsing, and applying parsed project path/docs configs to graph nodes. `src/project/fs.zig` owns deterministic resource file discovery, Linux directory traversal helpers, and resource path/name helpers. `src/project/jinja.zig` owns lexical Jinja call, parenthesis, quoted string, and literal argument helpers while graph-aware scanning remains in `src/project.zig`. `src/project/resolve.zig` owns graph lookup/count helpers, canonical graph resource ordering, duplicate resource validation, macro unique-id package extraction, low-level ref/source resolution helpers, and dependency-map mutation for refs, sources, and known macro dependencies. `src/project/parse.zig` owns narrow parser scalar helpers for YAML booleans, JSON-compatible scalar classification, generic-test YAML item names, generic-test definition construction/cloning, and generic-test identity/name/hash helpers with native tests. `src/project.zig` remains the public parser/list facade and still owns most loader, resource parser, scanner, and resolver orchestration code until follow-up extractions move those pieces behind focused internal modules.
572+
- M1A has started with behavior-preserving `src/project/types.zig`, `src/project/selector.zig`, `src/project/manifest.zig`, `src/project/util.zig`, `src/project/config.zig`, `src/project/fs.zig`, `src/project/jinja.zig`, `src/project/resolve.zig`, and `src/project/parse.zig` extractions. `src/project/manifest.zig` owns selected-resource JSON and partial `manifest.json` writing with native tests for selected JSON shape, JSON escaping, exposure dependency ordering, disabled-resource filtering, and graph-map output. `src/project/util.zig` owns shared display, membership, append-dedup, string sorting, and narrow YAML scalar/list helpers. `src/project/config.zig` owns `dbt_project.yml` loading, project path/docs config parsing, and applying parsed project path/docs configs to graph nodes. `src/project/fs.zig` owns deterministic resource file discovery, Linux directory traversal helpers, and resource path/name helpers. `src/project/jinja.zig` owns lexical Jinja call, parenthesis, quoted string, and literal argument helpers while graph-aware scanning remains in `src/project.zig`. `src/project/resolve.zig` owns graph lookup/count helpers, canonical graph resource ordering, duplicate resource validation, macro unique-id package extraction, low-level ref/source resolution helpers, and dependency-map mutation for refs, sources, and known macro dependencies. `src/project/parse.zig` owns narrow parser scalar helpers for YAML booleans, JSON-compatible scalar classification, generic-test YAML item names, generic-test definition construction/cloning, generic-test relationship target ref parsing, and generic-test identity/name/hash helpers with native tests. `src/project.zig` remains the public parser/list facade and still owns most loader, resource parser, scanner, and resolver orchestration code until follow-up extractions move those pieces behind focused internal modules.
573573
- `dxt parse` now targets the supported Tier 0 subset: project name/model paths/seed paths/macro paths, project and package model path configs for literal `+materialized`, `+tags`, and model/seed `+docs.node_color`, root-project model config overrides for installed packages, SQL model discovery, CSV seed discovery, installed package SQL model and CSV seed discovery from `dbt_packages`, source discovery, installed package source discovery, exposure discovery, installed package exposure discovery, project macro discovery, installed package macro discovery from `dbt_packages`, macro property YAML for project macro descriptions and arguments, project and package docs block discovery, literal `ref` to models or seeds, two-argument package refs, package-local refs in installed package models and exposures, unique installed-package fallback for unqualified refs, literal `source`, package-local sources in installed package models, unique installed-package fallback for unqualified sources, literal `doc` in project and package descriptions, inline `config(materialized=..., tags=...)`, known project/package-qualified/package-local macro call dependencies, narrow project and package YAML model properties for scalar descriptions, simple columns, tags, materialization, disabled SQL models, dbt-shaped `unique`, `not_null`, `accepted_values`, and `relationships` generic test nodes, model/test `refs` and `sources` artifact fields, dependency maps, and deterministic partial `manifest.json`. The manifest includes the v12 top-level maps needed by the M1 artifact shape and is covered by a pinned local dbt Manifest v12 schema slice. YAML generic test arguments are currently supported for scalar values plus inline and block lists required by public Jaffle Shop DuckDB-style tests.
574574
- `dxt ls` now lists dbt-selectable resources from the same parser graph with stable text/JSON output and basic name/FQN wildcards, tag wildcards, slash-aware `path:` wildcards, exact `package:`/`package:this`, `source:` wildcards including package-qualified source selectors, `exposure:` wildcards, `resource_type:`, `test_type:generic`, config materialization, comma intersection, whitespace union, multi-argument selector lists, repeated selector flags, leading/trailing `+` graph expansion, and exact exclude filters; macros are emitted in artifacts but not exposed as `ls` resources.
575575
- Synthetic fixtures cover one model, model refs, seed refs, source refs, exposure refs to models and sources, combined source/model YAML, inline config/tag selection, config materialization selection, comma-intersection selection, YAML model properties and columns, emitted `unique`, `not_null`, `accepted_values`, and `relationships` generic test nodes, project macro artifacts and macro properties, configured `macro-paths` replacing the default macro directory, installed package macros with package-qualified calls and package-local macro calls, installed package models, seeds, sources, docs, exposures, package YAML model properties, root package config overrides, and package-qualified/package-local refs/sources, macro calls recorded in model and macro `depends_on.macros`, docs blocks with literal `doc` descriptions, disabled models, disabled ref diagnostics, unmatched model-property warnings, duplicate model and docs diagnostics, unsupported dynamic ref/doc diagnostics, missing doc diagnostics, malformed docs block diagnostics, unresolved package macro diagnostics, and unsupported unknown macro-call diagnostics.

src/project.zig

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const SourceDef = types.SourceDef;
1818
const ExposureDef = types.ExposureDef;
1919
const MetaEntry = types.MetaEntry;
2020
const JsonScalar = types.JsonScalar;
21-
const RefDep = types.RefDep;
2221
const SourceDep = types.SourceDep;
2322
const ColumnDef = types.ColumnDef;
2423
const GenericTestDef = types.GenericTestDef;
@@ -56,6 +55,7 @@ const parseBool = project_parse.parseBool;
5655
const genericTestUniqueId = project_parse.genericTestUniqueId;
5756
const parseInlineGenericTestList = project_parse.parseInlineGenericTestList;
5857
const parseJsonScalar = project_parse.parseJsonScalar;
58+
const refDepFromValue = project_parse.refDepFromValue;
5959
const synthesizeGenericTestNames = project_parse.synthesizeGenericTestNames;
6060
const testNameFromYamlItem = project_parse.testNameFromYamlItem;
6161
const findKeyword = project_jinja.findKeyword;
@@ -1402,23 +1402,6 @@ fn currentGenericTestDef(graph: *Graph, model_index: usize, current_column: ?usi
14021402
return error.UnsupportedYaml;
14031403
}
14041404

1405-
fn refDepFromValue(allocator: std.mem.Allocator, value: []const u8) !RefDep {
1406-
const trimmed = std.mem.trim(u8, value, " \t\r");
1407-
if (std.mem.startsWith(u8, trimmed, "ref(")) {
1408-
const open = std.mem.indexOfScalar(u8, trimmed, '(') orelse return error.UnsupportedRef;
1409-
const close = findMatchingParen(trimmed, open) orelse return error.UnsupportedRef;
1410-
const args = std.mem.trim(u8, trimmed[open + 1 .. close], " \t\r");
1411-
var strings = try parseLiteralArgs(allocator, args, error.UnsupportedRef);
1412-
defer strings.deinit(allocator);
1413-
if (!(strings.items.len == 1 or strings.items.len == 2)) return error.UnsupportedRef;
1414-
return .{
1415-
.package = if (strings.items.len == 2) strings.items[0] else null,
1416-
.name = if (strings.items.len == 2) strings.items[1] else strings.items[0],
1417-
};
1418-
}
1419-
return .{ .package = null, .name = try dupTrimmedScalar(allocator, trimmed) };
1420-
}
1421-
14221405
fn sortGenericTestDefs(tests: []GenericTestDef) void {
14231406
std.mem.sort(GenericTestDef, tests, {}, struct {
14241407
fn lessThan(_: void, a: GenericTestDef, b: GenericTestDef) bool {

src/project/parse.zig

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
const std = @import("std");
2+
const jinja = @import("jinja.zig");
23
const types = @import("types.zig");
34
const util = @import("util.zig");
45

56
const JsonScalar = types.JsonScalar;
67
const GenericTestDef = types.GenericTestDef;
78
const Graph = types.Graph;
9+
const RefDep = types.RefDep;
810
const dupTrimmedScalar = util.dupTrimmedScalar;
11+
const findMatchingParen = jinja.findMatchingParen;
12+
const parseLiteralArgs = jinja.parseLiteralArgs;
913

1014
pub fn parseBool(value: []const u8) !bool {
1115
const trimmed = std.mem.trim(u8, value, " \t\r");
@@ -97,6 +101,23 @@ pub fn appendGenericTestDefClone(graph: *Graph, tests: *std.ArrayList(GenericTes
97101
try tests.append(graph.allocator, cloned);
98102
}
99103

104+
pub fn refDepFromValue(allocator: std.mem.Allocator, value: []const u8) !RefDep {
105+
const trimmed = std.mem.trim(u8, value, " \t\r");
106+
if (std.mem.startsWith(u8, trimmed, "ref(")) {
107+
const open = std.mem.indexOfScalar(u8, trimmed, '(') orelse return error.UnsupportedRef;
108+
const close = findMatchingParen(trimmed, open) orelse return error.UnsupportedRef;
109+
const args = std.mem.trim(u8, trimmed[open + 1 .. close], " \t\r");
110+
var strings = try parseLiteralArgs(allocator, args, error.UnsupportedRef);
111+
defer strings.deinit(allocator);
112+
if (!(strings.items.len == 1 or strings.items.len == 2)) return error.UnsupportedRef;
113+
return .{
114+
.package = if (strings.items.len == 2) strings.items[0] else null,
115+
.name = if (strings.items.len == 2) strings.items[1] else strings.items[0],
116+
};
117+
}
118+
return .{ .package = null, .name = try dupTrimmedScalar(allocator, trimmed) };
119+
}
120+
100121
pub const GenericTestNames = struct {
101122
full: []const u8,
102123
compiled: []const u8,
@@ -347,6 +368,34 @@ test "appendGenericTestDefClone copies nested accepted values list" {
347368
try std.testing.expectEqualStrings("returned", clones.items[0].accepted_values.items[1]);
348369
}
349370

371+
test "refDepFromValue parses relationship target refs and raw model names" {
372+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
373+
defer arena.deinit();
374+
const allocator = arena.allocator();
375+
376+
const raw = try refDepFromValue(allocator, " customers ");
377+
try std.testing.expect(raw.package == null);
378+
try std.testing.expectEqualStrings("customers", raw.name);
379+
380+
const local_ref = try refDepFromValue(allocator, "ref('orders')");
381+
try std.testing.expect(local_ref.package == null);
382+
try std.testing.expectEqualStrings("orders", local_ref.name);
383+
384+
const package_ref = try refDepFromValue(allocator, " ref(\"pkg\", 'orders') ");
385+
try std.testing.expectEqualStrings("pkg", package_ref.package.?);
386+
try std.testing.expectEqualStrings("orders", package_ref.name);
387+
}
388+
389+
test "refDepFromValue rejects unsupported dynamic or malformed refs" {
390+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
391+
defer arena.deinit();
392+
const allocator = arena.allocator();
393+
394+
try std.testing.expectError(error.UnsupportedRef, refDepFromValue(allocator, "ref(var('model'))"));
395+
try std.testing.expectError(error.UnsupportedRef, refDepFromValue(allocator, "ref('pkg', 'orders', 'extra')"));
396+
try std.testing.expectError(error.UnsupportedRef, refDepFromValue(allocator, "ref('orders'"));
397+
}
398+
350399
test "synthesizeGenericTestNames preserves short generic test identities" {
351400
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
352401
defer arena.deinit();

0 commit comments

Comments
 (0)