Skip to content

Commit bb41da8

Browse files
committed
feat(StructuredTable): add support for option type
1 parent 7d820f6 commit bb41da8

File tree

3 files changed

+172
-98
lines changed

3 files changed

+172
-98
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const allocator = std.heap.page_allocator;
5555
const Animal = struct {
5656
id: i32,
5757
name: []const u8,
58+
happy: ?bool,
5859
};
5960
6061
// Parse CSV data into a StructuredTable
@@ -64,10 +65,10 @@ var table = csv.StructuredTable(Animal).init(
6465
);
6566
defer table.deinit();
6667
try table.parse(
67-
\\id,name
68-
\\1,dog
69-
\\2,cat
70-
\\3,bird
68+
\\id,name,happy
69+
\\1,dog,
70+
\\2,cat,
71+
\\3,bird,
7172
);
7273
7374
// Modify the name of the animal with id 2
@@ -98,10 +99,10 @@ for (0..table.getRowCount()) |index| {
9899
const exported_csv = try table.exportCSV(allocator);
99100
defer allocator.free(exported_csv);
100101
std.debug.print("Exported CSV:\n{s}\n", .{exported_csv});
101-
// id,name
102-
// 1,dog
103-
// 2,mouse
104-
// 3,bird
102+
// id,name,happy
103+
// 1,dog,
104+
// 2,mouse,
105+
// 3,bird,
105106
106107
```
107108

src/schema.zig

Lines changed: 97 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub const StructureError = error{
2121
/// Result of parsing a row into a structured type
2222
/// Used to provide detailed error information when parsing fails
2323
pub fn ParseResult(table_schema: type) type {
24-
return union {
24+
return union(enum) {
2525
/// Successfully parsed structured value
2626
ok: struct {
2727
/// The parsed structured value
@@ -30,7 +30,7 @@ pub fn ParseResult(table_schema: type) type {
3030
/// Error occurred while parsing structured value
3131
@"error": struct {
3232
/// The kind of structure error that occurred
33-
kind: StructureError,
33+
kind: (StructureError || TableError),
3434
/// The name of the field that caused the error
3535
field_name: ?[]const u8,
3636
/// The expected type of the field that caused the error
@@ -95,11 +95,9 @@ pub fn StructuredTable(table_schema: type) type {
9595
/// Convert a data-row index to the corresponding underlying table index.
9696
///
9797
/// The underlying `Table` stores the header row at table index 0, while
98-
/// data rows start at 1. This helper maps a data-row index (or `null` to
99-
/// indicate append) to the `Table` insert index.
100-
fn headerAwareToTableIndex(data_index: ?usize) ?usize {
101-
// If `data_index` is null, that represents "append" — forward null to Table.insertEmptyRow.
102-
return if (data_index) |i| i + 1 else null;
98+
/// data rows start at 1. This helper maps a data-row index to the `Table` insert index.
99+
fn headerAwareToTableIndex(data_index: usize) usize {
100+
return data_index + 1;
103101
}
104102

105103
/// Convert an underlying table index to a data-row index.
@@ -110,6 +108,85 @@ pub fn StructuredTable(table_schema: type) type {
110108
return table_index - 1;
111109
}
112110

111+
/// Deserialize a CSV value into the appropriate field type
112+
fn deserializeCsvValue(self: Self, comptime T: type, value: []const u8) (TableError || StructureError)!T {
113+
const type_info = @typeInfo(T);
114+
if (type_info == .pointer and
115+
type_info.pointer.size == .slice and
116+
type_info.pointer.child == u8)
117+
{
118+
return value;
119+
}
120+
switch (type_info) {
121+
.optional => {
122+
const child_type = type_info.optional.child;
123+
if (value.len == 0) {
124+
return null;
125+
} else {
126+
return try self.deserializeCsvValue(child_type, value);
127+
}
128+
},
129+
.bool => {
130+
const lower = std.ascii.allocLowerString(self.allocator, value) catch return TableError.OutOfMemory;
131+
defer self.allocator.free(lower);
132+
for ([_][]const u8{ "true", "1", "yes", "y" }) |true_word| {
133+
if (std.mem.eql(u8, true_word, lower)) {
134+
return true;
135+
}
136+
}
137+
for ([_][]const u8{ "false", "0", "no", "n" }) |false_word| {
138+
if (std.mem.eql(u8, false_word, lower)) {
139+
return false;
140+
}
141+
}
142+
return StructureError.UnexpectedType;
143+
},
144+
.int => {
145+
return std.fmt.parseInt(T, value, 0) catch StructureError.UnexpectedType;
146+
},
147+
.float => {
148+
return std.fmt.parseFloat(T, value) catch StructureError.UnexpectedType;
149+
},
150+
else => {
151+
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(type_info)}));
152+
},
153+
}
154+
}
155+
156+
/// Serialize a field value into a CSV-compatible string
157+
fn serializeCsvValue(self: *Self, comptime T: type, value: T) TableError![]const u8 {
158+
const type_info = @typeInfo(T);
159+
if (type_info == .pointer and
160+
type_info.pointer.size == .slice and
161+
type_info.pointer.child == u8)
162+
{
163+
return value;
164+
}
165+
switch (type_info) {
166+
.optional => {
167+
const child_type = type_info.optional.child;
168+
if (value == null) {
169+
return "";
170+
} else {
171+
return try self.serializeCsvValue(child_type, value.?);
172+
}
173+
},
174+
.bool => {
175+
if (value) {
176+
return "true";
177+
} else {
178+
return "false";
179+
}
180+
},
181+
.int, .float => {
182+
return std.fmt.allocPrint(self.arena_allocator.allocator(), "{d}", .{value}) catch TableError.OutOfMemory;
183+
},
184+
else => {
185+
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(type_info)}));
186+
},
187+
}
188+
}
189+
113190
/// Get a structured row from the StructuredTable by index
114191
///
115192
/// Example looping through all rows:
@@ -131,7 +208,6 @@ pub fn StructuredTable(table_schema: type) type {
131208
var out: table_schema = undefined;
132209
inline for (schema_info.@"struct".fields) |field| {
133210
const field_name = field.name;
134-
const field_type = @typeInfo(field.type);
135211
const column_indexes = self.table.findColumnIndexesByValue(self.allocator, 0, field_name) catch return ParseResult(table_schema){
136212
.@"error" = .{
137213
.kind = StructureError.MissingColumn,
@@ -159,63 +235,15 @@ pub fn StructuredTable(table_schema: type) type {
159235
};
160236
defer self.allocator.free(rows);
161237
const value = rows[row_index + 1];
162-
if (field_type == .pointer and
163-
field_type.pointer.size == .slice and
164-
field_type.pointer.child == u8)
165-
{
166-
@field(out, field_name) = value;
167-
continue;
168-
}
169-
switch (field_type) {
170-
.bool => {
171-
const lower = std.ascii.allocLowerString(self.allocator, value) catch return TableError.OutOfMemory;
172-
defer self.allocator.free(lower);
173-
var matched = false;
174-
for ([_][]const u8{ "true", "1", "yes", "y" }) |true_word| {
175-
if (std.mem.eql(u8, true_word, lower)) {
176-
@field(out, field_name) = true;
177-
matched = true;
178-
}
179-
}
180-
for ([_][]const u8{ "false", "0", "no", "n" }) |false_word| {
181-
if (std.mem.eql(u8, false_word, lower)) {
182-
@field(out, field_name) = false;
183-
matched = true;
184-
}
185-
}
186-
if (!matched) return ParseResult(table_schema){
187-
.@"error" = .{
188-
.kind = StructureError.UnexpectedType,
189-
.field_name = field_name,
190-
.field_type = @typeName(field.type),
191-
.csv_value = value,
192-
},
193-
};
194-
},
195-
.int => {
196-
@field(out, field_name) = std.fmt.parseInt(field.type, value, 0) catch return ParseResult(table_schema){
197-
.@"error" = .{
198-
.kind = StructureError.UnexpectedType,
199-
.field_name = field_name,
200-
.field_type = @typeName(field.type),
201-
.csv_value = value,
202-
},
203-
};
204-
},
205-
.float => {
206-
@field(out, field_name) = std.fmt.parseFloat(field.type, value) catch return ParseResult(table_schema){
207-
.@"error" = .{
208-
.kind = StructureError.UnexpectedType,
209-
.field_name = field_name,
210-
.field_type = @typeName(field.type),
211-
.csv_value = value,
212-
},
213-
};
214-
},
215-
else => {
216-
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(field.type)}));
238+
const parsed = (&self).deserializeCsvValue(field.type, value) catch |err| return ParseResult(table_schema){
239+
.@"error" = .{
240+
.kind = err,
241+
.field_name = field_name,
242+
.field_type = @typeName(field.type),
243+
.csv_value = value,
217244
},
218-
}
245+
};
246+
@field(out, field_name) = parsed;
219247
}
220248
return ParseResult(table_schema){
221249
.ok = .{
@@ -240,7 +268,6 @@ pub fn StructuredTable(table_schema: type) type {
240268
if (row_index >= self.getRowCount()) return TableError.RowNotFound;
241269
inline for (schema_info.@"struct".fields) |field| {
242270
const field_name = field.name;
243-
const field_type = @typeInfo(field.type);
244271
const column_indexes = self.table.findColumnIndexesByValue(self.allocator, 0, field_name) catch return ParseResult(table_schema){
245272
.@"error" = .{
246273
.kind = StructureError.MissingColumn,
@@ -259,29 +286,9 @@ pub fn StructuredTable(table_schema: type) type {
259286
},
260287
};
261288
const column_index = column_indexes[0];
262-
if (field_type == .pointer and
263-
field_type.pointer.size == .slice and
264-
field_type.pointer.child == u8)
265-
{
266-
try self.table.replaceValue(row_index + 1, column_index, @field(row, field_name));
267-
continue;
268-
}
269-
switch (field_type) {
270-
.bool => {
271-
if (@field(row, field_name)) {
272-
try self.table.replaceValue(row_index + 1, column_index, "true");
273-
} else {
274-
try self.table.replaceValue(row_index + 1, column_index, "false");
275-
}
276-
},
277-
.int, .float => {
278-
const formatted = std.fmt.allocPrint(self.arena_allocator.allocator(), "{d}", .{@field(row, field_name)}) catch return TableError.OutOfMemory;
279-
try self.table.replaceValue(row_index + 1, column_index, formatted);
280-
},
281-
else => {
282-
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(field.type)}));
283-
},
284-
}
289+
const table_index = headerAwareToTableIndex(row_index);
290+
const value = try self.serializeCsvValue(field.type, @field(row, field_name));
291+
try self.table.replaceValue(table_index, column_index, value);
285292
}
286293
return ParseResult(table_schema){
287294
.ok = .{
@@ -306,8 +313,8 @@ pub fn StructuredTable(table_schema: type) type {
306313
try self.table.replaceValue(0, header_row_index, field.name);
307314
}
308315
}
309-
const table_insert_idx = headerAwareToTableIndex(row_index);
310-
const index = self.table.insertEmptyRow(table_insert_idx) catch return TableError.OutOfMemory;
316+
const table_index = if (row_index) |index| headerAwareToTableIndex(index) else null;
317+
const index = self.table.insertEmptyRow(table_index) catch return TableError.OutOfMemory;
311318
const data_index = headerAwareToDataIndex(index) orelse return TableError.RowNotFound;
312319
_ = try self.editRow(data_index, row);
313320
}

src/tests/schema.zig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,69 @@ test "StructuredTable: Handle parsing error due to invalid csv type" {
230230
try expect(std.mem.eql(u8, err.field_name.?, "age"));
231231
try expect(std.mem.eql(u8, err.field_type.?, "u8"));
232232
}
233+
234+
test "StructuredTable: Optional fields parse and null behavior" {
235+
const DogTableOpt = struct {
236+
name: ?[]const u8,
237+
age: ?u8,
238+
alive: ?bool,
239+
foo: ?f32,
240+
};
241+
242+
var table = StructuredTable(DogTableOpt).init(allocator, csv.Settings.default());
243+
defer table.deinit();
244+
try table.parse(
245+
\\name,age,alive,foo
246+
\\Fido,4,Yes,0.3
247+
\\,,,
248+
);
249+
250+
try expect(table.getRowCount() == 2);
251+
252+
const row_0 = try table.getRow(0);
253+
const value_0 = row_0.ok.value;
254+
try expect(std.mem.eql(u8, value_0.name.?, "Fido"));
255+
try expect(value_0.age.? == 4);
256+
try expect(value_0.alive.?);
257+
try expect(value_0.foo.? == 0.3);
258+
259+
const row_1 = try table.getRow(1);
260+
const value_1 = row_1.ok.value;
261+
try expect(value_1.name == null);
262+
try expect(value_1.age == null);
263+
try expect(value_1.alive == null);
264+
try expect(value_1.foo == null);
265+
}
266+
267+
test "StructuredTable: Optional fields edit writes empty when null" {
268+
const DogTableOpt = struct {
269+
name: ?[]const u8,
270+
age: ?u8,
271+
alive: ?bool,
272+
foo: ?f32,
273+
};
274+
275+
var table = StructuredTable(DogTableOpt).init(allocator, csv.Settings.default());
276+
defer table.deinit();
277+
try table.parse(
278+
\\name,age,alive,foo
279+
\\Fido,4,true,0.3
280+
);
281+
282+
const row = try table.getRow(0);
283+
var value = row.ok.value;
284+
value.name = null;
285+
value.age = null;
286+
value.alive = null;
287+
value.foo = null;
288+
289+
_ = try table.editRow(0, value);
290+
291+
const exported = try table.exportCSV(allocator);
292+
defer allocator.free(exported);
293+
const expected_csv =
294+
\\name,age,alive,foo
295+
\\,,,
296+
;
297+
try expect(std.mem.eql(u8, exported, expected_csv));
298+
}

0 commit comments

Comments
 (0)