Skip to content

Commit c159527

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

3 files changed

Lines changed: 168 additions & 93 deletions

File tree

README.md

Lines changed: 5 additions & 4 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

src/schema.zig

Lines changed: 97 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ 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+
/// Either a StructureError or TableError
34+
kind: anyerror,
3435
/// The name of the field that caused the error
3536
field_name: ?[]const u8,
3637
/// The expected type of the field that caused the error
@@ -95,11 +96,9 @@ pub fn StructuredTable(table_schema: type) type {
9596
/// Convert a data-row index to the corresponding underlying table index.
9697
///
9798
/// 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;
99+
/// data rows start at 1. This helper maps a data-row index to the `Table` insert index.
100+
fn headerAwareToTableIndex(data_index: usize) usize {
101+
return data_index + 1;
103102
}
104103

105104
/// Convert an underlying table index to a data-row index.
@@ -110,6 +109,85 @@ pub fn StructuredTable(table_schema: type) type {
110109
return table_index - 1;
111110
}
112111

112+
/// Deserialize a CSV value into the appropriate field type
113+
fn deserializeCsvValue(self: Self, comptime T: type, value: []const u8) (TableError || StructureError)!T {
114+
const type_info = @typeInfo(T);
115+
if (type_info == .pointer and
116+
type_info.pointer.size == .slice and
117+
type_info.pointer.child == u8)
118+
{
119+
return value;
120+
}
121+
switch (type_info) {
122+
.optional => {
123+
const child_type = type_info.optional.child;
124+
if (value.len == 0) {
125+
return null;
126+
} else {
127+
return try self.deserializeCsvValue(child_type, value);
128+
}
129+
},
130+
.bool => {
131+
const lower = std.ascii.allocLowerString(self.allocator, value) catch return TableError.OutOfMemory;
132+
defer self.allocator.free(lower);
133+
for ([_][]const u8{ "true", "1", "yes", "y" }) |true_word| {
134+
if (std.mem.eql(u8, true_word, lower)) {
135+
return true;
136+
}
137+
}
138+
for ([_][]const u8{ "false", "0", "no", "n" }) |false_word| {
139+
if (std.mem.eql(u8, false_word, lower)) {
140+
return false;
141+
}
142+
}
143+
return StructureError.UnexpectedType;
144+
},
145+
.int => {
146+
return std.fmt.parseInt(T, value, 0) catch StructureError.UnexpectedType;
147+
},
148+
.float => {
149+
return std.fmt.parseFloat(T, value) catch StructureError.UnexpectedType;
150+
},
151+
else => {
152+
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(type_info)}));
153+
},
154+
}
155+
}
156+
157+
/// Serialize a field value into a CSV-compatible string
158+
fn serializeCsvValue(self: *Self, comptime T: type, value: T) TableError![]const u8 {
159+
const type_info = @typeInfo(T);
160+
if (type_info == .pointer and
161+
type_info.pointer.size == .slice and
162+
type_info.pointer.child == u8)
163+
{
164+
return value;
165+
}
166+
switch (type_info) {
167+
.optional => {
168+
const child_type = type_info.optional.child;
169+
if (value == null) {
170+
return "";
171+
} else {
172+
return try self.serializeCsvValue(child_type, value.?);
173+
}
174+
},
175+
.bool => {
176+
if (value) {
177+
return "true";
178+
} else {
179+
return "false";
180+
}
181+
},
182+
.int, .float => {
183+
return std.fmt.allocPrint(self.arena_allocator.allocator(), "{d}", .{value}) catch TableError.OutOfMemory;
184+
},
185+
else => {
186+
@compileError(std.fmt.comptimePrint("unsupported field type for '{}'", .{@typeName(type_info)}));
187+
},
188+
}
189+
}
190+
113191
/// Get a structured row from the StructuredTable by index
114192
///
115193
/// Example looping through all rows:
@@ -131,7 +209,6 @@ pub fn StructuredTable(table_schema: type) type {
131209
var out: table_schema = undefined;
132210
inline for (schema_info.@"struct".fields) |field| {
133211
const field_name = field.name;
134-
const field_type = @typeInfo(field.type);
135212
const column_indexes = self.table.findColumnIndexesByValue(self.allocator, 0, field_name) catch return ParseResult(table_schema){
136213
.@"error" = .{
137214
.kind = StructureError.MissingColumn,
@@ -159,63 +236,15 @@ pub fn StructuredTable(table_schema: type) type {
159236
};
160237
defer self.allocator.free(rows);
161238
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)}));
239+
const parsed = (&self).deserializeCsvValue(field.type, value) catch |err| return ParseResult(table_schema){
240+
.@"error" = .{
241+
.kind = err,
242+
.field_name = field_name,
243+
.field_type = @typeName(field.type),
244+
.csv_value = value,
217245
},
218-
}
246+
};
247+
@field(out, field_name) = parsed;
219248
}
220249
return ParseResult(table_schema){
221250
.ok = .{
@@ -240,7 +269,6 @@ pub fn StructuredTable(table_schema: type) type {
240269
if (row_index >= self.getRowCount()) return TableError.RowNotFound;
241270
inline for (schema_info.@"struct".fields) |field| {
242271
const field_name = field.name;
243-
const field_type = @typeInfo(field.type);
244272
const column_indexes = self.table.findColumnIndexesByValue(self.allocator, 0, field_name) catch return ParseResult(table_schema){
245273
.@"error" = .{
246274
.kind = StructureError.MissingColumn,
@@ -259,29 +287,9 @@ pub fn StructuredTable(table_schema: type) type {
259287
},
260288
};
261289
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-
}
290+
const table_index = headerAwareToTableIndex(row_index);
291+
const value = try self.serializeCsvValue(field.type, @field(row, field_name));
292+
try self.table.replaceValue(table_index, column_index, value);
285293
}
286294
return ParseResult(table_schema){
287295
.ok = .{
@@ -306,8 +314,8 @@ pub fn StructuredTable(table_schema: type) type {
306314
try self.table.replaceValue(0, header_row_index, field.name);
307315
}
308316
}
309-
const table_insert_idx = headerAwareToTableIndex(row_index);
310-
const index = self.table.insertEmptyRow(table_insert_idx) catch return TableError.OutOfMemory;
317+
const table_index = if (row_index) |index| headerAwareToTableIndex(index) else null;
318+
const index = self.table.insertEmptyRow(table_index) catch return TableError.OutOfMemory;
311319
const data_index = headerAwareToDataIndex(index) orelse return TableError.RowNotFound;
312320
_ = try self.editRow(data_index, row);
313321
}

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)