Clean up db models
This commit is contained in:
parent
f6f24a557e
commit
99192bccdd
4 changed files with 136 additions and 85 deletions
|
@ -156,34 +156,31 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getAuthenticatedUser(self: *Self) !models.LocalUser {
|
fn getAuthenticatedLocalUser(self: *Self) !models.LocalUser {
|
||||||
if (self.as_user) |user_id| {
|
if (self.as_user) |user_id| {
|
||||||
const local_user = try self.db.getBy(models.LocalUser, .id, user_id, self.arena.allocator());
|
const local_user = try self.db.getBy(models.LocalUser, .user_id, user_id, self.arena.allocator());
|
||||||
if (local_user == null) return error.UserNotFound;
|
if (local_user == null) return error.NotAuthorized;
|
||||||
|
|
||||||
return local_user.?;
|
return local_user.?;
|
||||||
} else {
|
} else {
|
||||||
return error.NotAuthenticated;
|
return error.NotAuthorized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getAuthenticatedActor(self: *Self) !models.Actor {
|
fn getAuthenticatedActor(self: *Self) !models.Actor {
|
||||||
const user = try self.getAuthenticatedUser();
|
return if (self.as_user) |user_id|
|
||||||
if (user.actor_id) |actor_id| {
|
(try self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator())) orelse error.NotAuthorized
|
||||||
const actor = try self.db.getBy(models.Actor, .id, actor_id, self.arena);
|
else
|
||||||
return actor.?;
|
error.NotAuthorized;
|
||||||
} else {
|
|
||||||
return error.NoActor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note {
|
pub fn createNote(self: *Self, info: NoteCreateInfo) !models.Note {
|
||||||
const id = Uuid.randV4(prng.random());
|
const id = Uuid.randV4(prng.random());
|
||||||
const user = try self.getAuthenticatedUser();
|
const actor = try self.getAuthenticatedActor();
|
||||||
|
|
||||||
const note = models.Note{
|
const note = models.Note{
|
||||||
.id = id,
|
.id = id,
|
||||||
.author_id = user.actor_id orelse return error.NotAuthorized,
|
.author_id = actor.user_id,
|
||||||
.content = info.content,
|
.content = info.content,
|
||||||
|
|
||||||
.created_at = DateTime.now(),
|
.created_at = DateTime.now(),
|
||||||
|
@ -197,18 +194,19 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
return self.db.getBy(models.Note, .id, id, self.arena.allocator());
|
return self.db.getBy(models.Note, .id, id, self.arena.allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getActor(self: *Self, id: Uuid) !?models.Actor {
|
pub fn getActor(self: *Self, user_id: Uuid) !?models.Actor {
|
||||||
return self.db.getBy(models.Actor, .id, id, self.arena.allocator());
|
return self.db.getBy(models.Actor, .user_id, user_id, self.arena.allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getActorByHandle(self: *Self, handle: []const u8) !?models.Actor {
|
pub fn getActorByHandle(self: *Self, handle: []const u8) !?models.Actor {
|
||||||
return self.db.getBy(models.Actor, .handle, handle, self.arena.allocator());
|
const user = (try self.db.getBy(models.User, .username, handle, self.arena.allocator())) orelse return null;
|
||||||
|
return self.db.getBy(models.Actor, .user_id, user.id, self.arena.allocator());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn react(self: *Self, note_id: Uuid) !void {
|
pub fn react(self: *Self, note_id: Uuid) !void {
|
||||||
const id = Uuid.randV4(prng.random());
|
const id = Uuid.randV4(prng.random());
|
||||||
const user = try self.getAuthenticatedUser();
|
const actor = try self.getAuthenticatedActor();
|
||||||
try self.db.insert(models.Reaction, .{ .id = id, .note_id = note_id, .reactor_id = user.actor_id orelse return error.NotAuthorized, .created_at = DateTime.now() });
|
try self.db.insert(models.Reaction, .{ .id = id, .note_id = note_id, .reactor_id = actor.user_id, .created_at = DateTime.now() });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listReacts(self: *Self, note_id: Uuid) ![]models.Reaction {
|
pub fn listReacts(self: *Self, note_id: Uuid) ![]models.Reaction {
|
||||||
|
@ -216,18 +214,13 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register(self: *Self, info: RegistrationInfo) !models.Actor {
|
pub fn register(self: *Self, info: RegistrationInfo) !models.Actor {
|
||||||
const actor_id = Uuid.randV4(prng.random());
|
|
||||||
const user_id = Uuid.randV4(prng.random());
|
const user_id = Uuid.randV4(prng.random());
|
||||||
// TODO: lock for transaction
|
// TODO: lock for transaction
|
||||||
|
|
||||||
if (try self.db.existsWhereEq(models.LocalUser, .username, info.username)) {
|
if (try self.db.existsWhereEq(models.User, .username, info.username)) {
|
||||||
return error.UsernameUnavailable;
|
return error.UsernameUnavailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try self.db.existsWhereEq(models.Actor, .handle, info.username)) {
|
|
||||||
return error.InconsistentDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const invite_id = if (info.invite_code) |invite_code| blk: {
|
const invite_id = if (info.invite_code) |invite_code| blk: {
|
||||||
const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
|
const invite = (try self.db.getBy(models.Invite, .invite_code, invite_code, self.arena.allocator())) orelse return error.InvalidInvite;
|
||||||
|
@ -244,39 +237,42 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
var buf: [pw_hash_buf_size]u8 = undefined;
|
var buf: [pw_hash_buf_size]u8 = undefined;
|
||||||
const hash = try PwHash.strHash(info.password, .{ .allocator = self.internal_alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf);
|
const hash = try PwHash.strHash(info.password, .{ .allocator = self.internal_alloc, .params = pw_hash_params, .encoding = pw_hash_encoding }, &buf);
|
||||||
|
|
||||||
const actor = models.Actor{
|
const user = models.User{
|
||||||
.id = actor_id,
|
.id = user_id,
|
||||||
.handle = info.username,
|
.username = info.username,
|
||||||
.created_at = now,
|
.created_at = now,
|
||||||
};
|
};
|
||||||
const user = models.LocalUser{
|
const actor = models.Actor{
|
||||||
.id = user_id,
|
.user_id = user_id,
|
||||||
.actor_id = actor_id,
|
.public_id = "abc", // TODO
|
||||||
.username = info.username,
|
};
|
||||||
|
const local_user = models.LocalUser{
|
||||||
|
.user_id = user_id,
|
||||||
.email = info.email,
|
.email = info.email,
|
||||||
.invite_id = invite_id,
|
.invite_id = invite_id,
|
||||||
.hashed_password = hash,
|
.hashed_password = hash,
|
||||||
.password_changed_at = now,
|
.password_changed_at = now,
|
||||||
.created_at = now,
|
|
||||||
};
|
};
|
||||||
|
try self.db.insert(models.User, user);
|
||||||
try self.db.insert(models.Actor, actor);
|
try self.db.insert(models.Actor, actor);
|
||||||
try self.db.insert(models.LocalUser, user);
|
try self.db.insert(models.LocalUser, local_user);
|
||||||
|
|
||||||
return actor;
|
return actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
|
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResult {
|
||||||
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
|
// TODO: This gives away the existence of a user through a timing side channel. is that acceptable?
|
||||||
const user_info = (try self.db.getBy(models.LocalUser, .username, username, self.arena.allocator())) orelse return error.InvalidLogin;
|
const user_info = (try self.db.getBy(models.User, .username, username, self.arena.allocator())) orelse return error.InvalidLogin;
|
||||||
|
const local_user_info = (try self.db.getBy(models.LocalUser, .user_id, user_info.id, self.arena.allocator())) orelse return error.InvalidLogin;
|
||||||
//defer free(self.arena.allocator(), user_info);
|
//defer free(self.arena.allocator(), user_info);
|
||||||
|
|
||||||
const Hash = std.crypto.pwhash.scrypt;
|
const Hash = std.crypto.pwhash.scrypt;
|
||||||
Hash.strVerify(user_info.hashed_password, password, .{ .allocator = self.internal_alloc }) catch |err| switch (err) {
|
Hash.strVerify(local_user_info.hashed_password, password, .{ .allocator = self.internal_alloc }) catch |err| switch (err) {
|
||||||
error.PasswordVerificationFailed => return error.InvalidLogin,
|
error.PasswordVerificationFailed => return error.InvalidLogin,
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = try self.createToken(user_info);
|
const token = try self.createToken(user_info.id);
|
||||||
|
|
||||||
var token_enc: [token_str_len]u8 = undefined;
|
var token_enc: [token_str_len]u8 = undefined;
|
||||||
_ = std.base64.standard.Encoder.encode(&token_enc, &token.value);
|
_ = std.base64.standard.Encoder.encode(&token_enc, &token.value);
|
||||||
|
@ -292,7 +288,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
info: models.Token,
|
info: models.Token,
|
||||||
value: [token_len]u8,
|
value: [token_len]u8,
|
||||||
};
|
};
|
||||||
fn createToken(self: *Self, user: models.LocalUser) !TokenResult {
|
fn createToken(self: *Self, user_id: Uuid) !TokenResult {
|
||||||
var token: [token_len]u8 = undefined;
|
var token: [token_len]u8 = undefined;
|
||||||
std.crypto.random.bytes(&token);
|
std.crypto.random.bytes(&token);
|
||||||
|
|
||||||
|
@ -302,7 +298,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
const db_token = models.Token{
|
const db_token = models.Token{
|
||||||
.id = Uuid.randV4(prng.random()),
|
.id = Uuid.randV4(prng.random()),
|
||||||
.hash = .{ .data = hash },
|
.hash = .{ .data = hash },
|
||||||
.user_id = user.id,
|
.user_id = user_id,
|
||||||
.issued_at = DateTime.now(),
|
.issued_at = DateTime.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -316,7 +312,7 @@ fn ApiConn(comptime DbConn: type) type {
|
||||||
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
|
pub fn createInvite(self: *Self, options: InviteOptions) !models.Invite {
|
||||||
const id = Uuid.randV4(prng.random());
|
const id = Uuid.randV4(prng.random());
|
||||||
|
|
||||||
const user_id = (try self.getAuthenticatedUser()).id;
|
const user_id = (try self.getAuthenticatedLocalUser()).user_id;
|
||||||
|
|
||||||
var code: [invite_code_len]u8 = undefined;
|
var code: [invite_code_len]u8 = undefined;
|
||||||
std.crypto.random.bytes(&code);
|
std.crypto.random.bytes(&code);
|
||||||
|
|
|
@ -8,11 +8,25 @@ const DateTime = util.DateTime;
|
||||||
const String = []const u8;
|
const String = []const u8;
|
||||||
const comptimePrint = std.fmt.comptimePrint;
|
const comptimePrint = std.fmt.comptimePrint;
|
||||||
|
|
||||||
|
fn baseTypeName(comptime T: type) []const u8 {
|
||||||
|
comptime {
|
||||||
|
const name = @typeName(T);
|
||||||
|
const start = for (name) |_, i| {
|
||||||
|
if (name[name.len - i] == '.') break name.len - i;
|
||||||
|
} else 0;
|
||||||
|
|
||||||
|
return name[start..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn tableName(comptime T: type) String {
|
fn tableName(comptime T: type) String {
|
||||||
|
//return util.case.pascalToSnake(baseTypeName(T));
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
models.Note => "note",
|
models.Note => "note",
|
||||||
models.Actor => "actor",
|
models.Actor => "actor",
|
||||||
models.Reaction => "reaction",
|
models.Reaction => "reaction",
|
||||||
|
models.User => "user",
|
||||||
models.LocalUser => "local_user",
|
models.LocalUser => "local_user",
|
||||||
models.Token => "token",
|
models.Token => "token",
|
||||||
models.Invite => "invite",
|
models.Invite => "invite",
|
||||||
|
@ -138,33 +152,28 @@ pub const Database = struct {
|
||||||
db: sql.Sqlite,
|
db: sql.Sqlite,
|
||||||
|
|
||||||
const init_sql_stmts = [_][]const u8{
|
const init_sql_stmts = [_][]const u8{
|
||||||
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
\\user(
|
||||||
|
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
\\ username TEXT NOT NULL,
|
||||||
|
\\
|
||||||
|
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
\\) STRICT;
|
||||||
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
\\actor(
|
\\actor(
|
||||||
\\ id TEXT NOT NULL,
|
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||||
\\
|
\\ public_id TEXT NOT NULL
|
||||||
\\ handle TEXT NOT NULL,
|
|
||||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
\\
|
|
||||||
\\ PRIMARY KEY(id)
|
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
\\local_user(
|
\\local_user(
|
||||||
\\ id TEXT NOT NULL,
|
\\ user_id TEXT NOT NULL PRIMARY KEY REFERENCES user(id),
|
||||||
\\ actor_id TEXT,
|
|
||||||
\\
|
\\
|
||||||
\\ username TEXT NOT NULL,
|
|
||||||
\\ email TEXT,
|
\\ email TEXT,
|
||||||
\\
|
\\
|
||||||
\\ hashed_password TEXT NOT NULL,
|
\\ hashed_password TEXT NOT NULL,
|
||||||
\\ password_changed_at INTEGER NOT NULL,
|
\\ password_changed_at INTEGER NOT NULL
|
||||||
\\
|
|
||||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
\\
|
|
||||||
\\ UNIQUE(actor_id),
|
|
||||||
\\ FOREIGN KEY(actor_id) REFERENCES actor(id),
|
|
||||||
\\
|
|
||||||
\\ PRIMARY KEY(id)
|
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
@ -172,38 +181,29 @@ pub const Database = struct {
|
||||||
\\ id TEXT NOT NULL,
|
\\ id TEXT NOT NULL,
|
||||||
\\
|
\\
|
||||||
\\ content TEXT NOT NULL,
|
\\ content TEXT NOT NULL,
|
||||||
\\ author_id TEXT NOT NULL,
|
\\ author_id TEXT NOT NULL REFERENCES actor(id),
|
||||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
\\
|
\\
|
||||||
\\ FOREIGN KEY(author_id) REFERENCES actor(id),
|
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
\\
|
|
||||||
\\ PRIMARY KEY(id)
|
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
\\reaction(
|
\\reaction(
|
||||||
\\ id TEXT NOT NULL,
|
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||||
\\
|
\\
|
||||||
\\ reactor_id TEXT NOT NULL,
|
\\ reactor_id TEXT NOT NULL REFERENCES actor(id),
|
||||||
\\ note_id TEXT NOT NULL,
|
\\ note_id TEXT NOT NULL REFERENCES note(id),
|
||||||
\\
|
\\
|
||||||
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
\\
|
|
||||||
\\ FOREIGN KEY(reactor_id) REFERENCES actor(id),
|
|
||||||
\\ FOREIGN KEY(note_id) REFERENCES note(id),
|
|
||||||
\\
|
|
||||||
\\ PRIMARY KEY(id)
|
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
\\token(
|
\\token(
|
||||||
\\ id TEXT NOT NULL,
|
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||||
\\
|
\\
|
||||||
\\ hash BLOB UNIQUE NOT NULL,
|
\\ hash BLOB UNIQUE NOT NULL,
|
||||||
\\ user_id TEXT NOT NULL REFERENCES local_user(id),
|
\\ user_id TEXT NOT NULL REFERENCES local_user(id),
|
||||||
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
\\
|
\\
|
||||||
\\ PRIMARY KEY(id)
|
\\ issued_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
\\CREATE TABLE IF NOT EXISTS
|
\\CREATE TABLE IF NOT EXISTS
|
||||||
|
@ -211,12 +211,12 @@ pub const Database = struct {
|
||||||
\\ id TEXT NOT NULL PRIMARY KEY,
|
\\ id TEXT NOT NULL PRIMARY KEY,
|
||||||
\\
|
\\
|
||||||
\\ name TEXT NOT NULL,
|
\\ name TEXT NOT NULL,
|
||||||
\\ invite_code TEXT NOT NULL,
|
\\ invite_code TEXT NOT NULL UNIQUE,
|
||||||
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
|
\\ created_by TEXT NOT NULL REFERENCES local_user(id),
|
||||||
\\
|
\\
|
||||||
\\ max_uses INTEGER,
|
\\ max_uses INTEGER,
|
||||||
\\
|
\\
|
||||||
\\ created_at INTEGER NOT NULL,
|
\\ created_at INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
\\ expires_at INTEGER
|
\\ expires_at INTEGER
|
||||||
\\) STRICT;
|
\\) STRICT;
|
||||||
,
|
,
|
||||||
|
|
|
@ -56,31 +56,32 @@ fn Ref(comptime _: type) type {
|
||||||
return Uuid;
|
return Uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Note = struct {
|
pub const User = struct {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
content: []const u8,
|
username: []const u8,
|
||||||
author_id: Ref(Actor),
|
|
||||||
|
|
||||||
created_at: DateTime,
|
created_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Actor = struct {
|
pub const Actor = struct {
|
||||||
id: Uuid,
|
user_id: Ref(User),
|
||||||
handle: []const u8,
|
public_id: []const u8,
|
||||||
|
|
||||||
created_at: DateTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const LocalUser = struct {
|
pub const LocalUser = struct {
|
||||||
id: Uuid,
|
user_id: Ref(User),
|
||||||
actor_id: ?Ref(Actor),
|
|
||||||
|
|
||||||
username: []const u8,
|
|
||||||
email: ?[]const u8,
|
email: ?[]const u8,
|
||||||
invite_id: ?Ref(Invite),
|
invite_id: ?Ref(Invite),
|
||||||
|
|
||||||
hashed_password: []const u8, // encoded in PHC format, with salt
|
hashed_password: []const u8, // encoded in PHC format, with salt
|
||||||
password_changed_at: DateTime,
|
password_changed_at: DateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Note = struct {
|
||||||
|
id: Uuid,
|
||||||
|
content: []const u8,
|
||||||
|
author_id: Ref(Actor),
|
||||||
|
|
||||||
created_at: DateTime,
|
created_at: DateTime,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,62 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
pub const ciutf8 = @import("./ciutf8.zig");
|
pub const ciutf8 = @import("./ciutf8.zig");
|
||||||
pub const Uuid = @import("./Uuid.zig");
|
pub const Uuid = @import("./Uuid.zig");
|
||||||
pub const DateTime = @import("./DateTime.zig");
|
pub const DateTime = @import("./DateTime.zig");
|
||||||
pub const PathIter = @import("./PathIter.zig");
|
pub const PathIter = @import("./PathIter.zig");
|
||||||
|
|
||||||
|
pub const case = struct {
|
||||||
|
// returns the number of capital letters in a string.
|
||||||
|
// only works with ascii characters
|
||||||
|
fn countCaps(str: []const u8) usize {
|
||||||
|
var count: usize = 0;
|
||||||
|
for (str) |ch| {
|
||||||
|
if (std.ascii.isUpper(ch)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts a string from PascalCase to snake_case at comptime.
|
||||||
|
// only works with ascii characters
|
||||||
|
pub fn PascalToSnake(comptime str: []const u8) Return: {
|
||||||
|
break :Return if (str.len == 0)
|
||||||
|
*const [0:0]u8
|
||||||
|
else
|
||||||
|
*const [str.len + countCaps(str) - 1:0]u8;
|
||||||
|
} {
|
||||||
|
comptime {
|
||||||
|
if (str.len == 0) return "";
|
||||||
|
|
||||||
|
var buf = std.mem.zeroes([str.len + countCaps(str) - 1:0]u8);
|
||||||
|
var i = 0;
|
||||||
|
for (str) |ch| {
|
||||||
|
if (std.ascii.isUpper(ch)) {
|
||||||
|
if (i != 0) {
|
||||||
|
buf[i] = '_';
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
buf[i] = std.ascii.toLower(ch);
|
||||||
|
} else {
|
||||||
|
buf[i] = ch;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "pascalToSnake" {
|
||||||
|
try std.testing.expectEqual("", case.PascalToSnake(""));
|
||||||
|
try std.testing.expectEqual("abc", case.PascalToSnake("Abc"));
|
||||||
|
try std.testing.expectEqual("a_bc", case.PascalToSnake("ABc"));
|
||||||
|
try std.testing.expectEqual("a_b_c", case.PascalToSnake("ABC"));
|
||||||
|
try std.testing.expectEqual("ab_c", case.PascalToSnake("AbC"));
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = ciutf8;
|
_ = ciutf8;
|
||||||
_ = Uuid;
|
_ = Uuid;
|
||||||
|
|
Loading…
Reference in a new issue