summaryrefslogtreecommitdiff
path: root/src/config/RepeatableStringMap.zig
blob: 6f143e95d79f4f066b38bfc4a2e72425665c8f5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/// RepeatableStringMap is a key/value that can be repeated to accumulate a
/// string map. This isn't called "StringMap" because I find that sometimes
/// leads to confusion that it _accepts_ a map such as JSON dict.
const RepeatableStringMap = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;

const formatterpkg = @import("formatter.zig");

const Map = std.ArrayHashMapUnmanaged(
    [:0]const u8,
    [:0]const u8,
    std.array_hash_map.StringContext,
    true,
);

// Allocator for the list is the arena for the parent config.
map: Map = .{},

pub fn parseCLI(
    self: *RepeatableStringMap,
    alloc: Allocator,
    input: ?[]const u8,
) !void {
    const value = input orelse return error.ValueRequired;

    // Empty value resets the list. We don't need to free our values because
    // the allocator used is always an arena.
    if (value.len == 0) {
        self.map.clearRetainingCapacity();
        return;
    }

    const index = std.mem.indexOfScalar(
        u8,
        value,
        '=',
    ) orelse return error.ValueRequired;

    const key = std.mem.trim(u8, value[0..index], &std.ascii.whitespace);
    const val = std.mem.trim(u8, value[index + 1 ..], &std.ascii.whitespace);

    const key_copy = try alloc.dupeZ(u8, key);
    errdefer alloc.free(key_copy);

    // Empty value removes the key from the map.
    if (val.len == 0) {
        _ = self.map.orderedRemove(key_copy);
        alloc.free(key_copy);
        return;
    }

    const val_copy = try alloc.dupeZ(u8, val);
    errdefer alloc.free(val_copy);

    try self.map.put(alloc, key_copy, val_copy);
}

/// Deep copy of the struct. Required by Config.
pub fn clone(
    self: *const RepeatableStringMap,
    alloc: Allocator,
) Allocator.Error!RepeatableStringMap {
    var map: Map = .{};
    try map.ensureTotalCapacity(alloc, self.map.count());

    errdefer {
        var it = map.iterator();
        while (it.next()) |entry| {
            alloc.free(entry.key_ptr.*);
            alloc.free(entry.value_ptr.*);
        }
        map.deinit(alloc);
    }

    var it = self.map.iterator();
    while (it.next()) |entry| {
        const key = try alloc.dupeZ(u8, entry.key_ptr.*);
        const value = try alloc.dupeZ(u8, entry.value_ptr.*);
        map.putAssumeCapacity(key, value);
    }

    return .{ .map = map };
}

/// The number of items in the map
pub fn count(self: RepeatableStringMap) usize {
    return self.map.count();
}

/// Iterator over the entries in the map.
pub fn iterator(self: RepeatableStringMap) Map.Iterator {
    return self.map.iterator();
}

/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: RepeatableStringMap, other: RepeatableStringMap) bool {
    if (self.map.count() != other.map.count()) return false;
    var it = self.map.iterator();
    while (it.next()) |entry| {
        const value = other.map.get(entry.key_ptr.*) orelse return false;
        if (!std.mem.eql(u8, entry.value_ptr.*, value)) return false;
    } else return true;
}

/// Used by formatter
pub fn formatEntry(self: RepeatableStringMap, formatter: anytype) !void {
    // If no items, we want to render an empty field.
    if (self.map.count() == 0) {
        try formatter.formatEntry(void, {});
        return;
    }

    var it = self.map.iterator();
    while (it.next()) |entry| {
        var buf: [256]u8 = undefined;
        const value = std.fmt.bufPrint(&buf, "{s}={s}", .{ entry.key_ptr.*, entry.value_ptr.* }) catch |err| switch (err) {
            error.NoSpaceLeft => return error.OutOfMemory,
        };
        try formatter.formatEntry([]const u8, value);
    }
}

test "RepeatableStringMap: parseCLI" {
    const testing = std.testing;
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var map: RepeatableStringMap = .{};

    try testing.expectError(error.ValueRequired, map.parseCLI(alloc, "A"));

    try map.parseCLI(alloc, "A=B");
    try map.parseCLI(alloc, "B=C");
    try testing.expectEqual(@as(usize, 2), map.count());

    try map.parseCLI(alloc, "");
    try testing.expectEqual(@as(usize, 0), map.count());

    try map.parseCLI(alloc, "A=B");
    try testing.expectEqual(@as(usize, 1), map.count());
    try map.parseCLI(alloc, "A=C");
    try testing.expectEqual(@as(usize, 1), map.count());
}

test "RepeatableStringMap: formatConfig empty" {
    const testing = std.testing;
    var buf = std.ArrayList(u8).init(testing.allocator);
    defer buf.deinit();

    var list: RepeatableStringMap = .{};
    try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
    try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
}

test "RepeatableStringMap: formatConfig single item" {
    const testing = std.testing;

    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    {
        var buf = std.ArrayList(u8).init(testing.allocator);
        defer buf.deinit();
        var map: RepeatableStringMap = .{};
        try map.parseCLI(alloc, "A=B");
        try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
        try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
    }
    {
        var buf = std.ArrayList(u8).init(testing.allocator);
        defer buf.deinit();
        var map: RepeatableStringMap = .{};
        try map.parseCLI(alloc, " A = B ");
        try map.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
        try std.testing.expectEqualSlices(u8, "a = A=B\n", buf.items);
    }
}

test "RepeatableStringMap: formatConfig multiple items" {
    const testing = std.testing;

    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    {
        var buf = std.ArrayList(u8).init(testing.allocator);
        defer buf.deinit();
        var list: RepeatableStringMap = .{};
        try list.parseCLI(alloc, "A=B");
        try list.parseCLI(alloc, "B = C");
        try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
        try std.testing.expectEqualSlices(u8, "a = A=B\na = B=C\n", buf.items);
    }
}