finnow/finnow-api/source/util/data.d

139 lines
4.0 KiB
D

module util.data;
import handy_http_primitives;
import handy_http_data.multipart;
import std.typecons;
import std.datetime;
Optional!T toOptional(T)(Nullable!T value) {
if (value.isNull) {
return Optional!T.empty;
} else {
return Optional!T.of(value.get);
}
}
Nullable!T toNullable(T)(Optional!T value) {
if (value.isNull) {
return Nullable!(T)();
} else {
return Nullable!T(value.value);
}
}
auto serializeOptional(T)(Optional!T value) {
if (value.isNull) {
return Nullable!T();
}
return Nullable!T(value.value);
}
T getPathParamOrThrow(T)(in ServerHttpRequest req, string name) {
import handy_http_handlers.path_handler;
import std.conv : to, ConvException;
foreach (param; getPathParams(req)) {
if (param.name == name) {
try {
return to!T(param.value);
} catch (ConvException e) {
// Skip and throw if no params match.
}
}
}
// No params matched, so throw a NOT FOUND error.
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Missing required path parameter \"" ~ name ~ "\".");
}
struct MultipartFile {
string name;
string contentType;
ubyte[] content;
}
struct MultipartFilesAndBody(T) {
T payload;
MultipartFile[] files;
}
MultipartFilesAndBody!T parseMultipartFilesAndBody(T)(
ref ServerHttpRequest request,
string payloadPartName = "payload",
string filesPartName = "file"
) {
import asdf : deserialize, SerdeException;
import std.uni : toLower;
import std.array;
MultipartFormData formData;
try {
formData = readBodyAsMultipartFormData(request);
} catch (MultipartFormatException e) {
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid multipart form data.");
}
MultipartFilesAndBody!T result;
RefAppender!(MultipartFile[]) app = appender(&result.files);
foreach (element; formData.elements) {
if (toLower(element.name) == payloadPartName) {
try {
result.payload = deserialize!T(element.content);
} catch (SerdeException e) {
throw new HttpStatusException(HttpStatus.BAD_REQUEST, e.msg);
}
} else if (toLower(element.name) == filesPartName) {
app ~= parseMultipartFile(element);
}
}
return result;
}
private MultipartFile parseMultipartFile(in MultipartElement e) {
import std.algorithm : splitter, count;
import std.array;
import std.string : strip;
const requiredHeaders = ["Content-Disposition", "Content-Type"];
static foreach (headerName; requiredHeaders) {
if (headerName !in e.headers || e.headers[headerName].length < 1) {
throw new HttpStatusException(
HttpStatus.BAD_REQUEST,
"Missing required \"" ~ headerName ~ "\" header for multipart file."
);
}
}
if ("Content-Disposition" !in e.headers) {
throw new HttpStatusException(
HttpStatus.BAD_REQUEST,
"Missing required \"Content-Disposition\" header for multipart file."
);
}
string filename;
foreach (part; e.headers["Content-Disposition"].splitter(";")) {
string[] partSegments = part.splitter("=").array;
if (count(partSegments) == 2 && partSegments[0].strip() == "filename") {
filename = partSegments[1].strip().strip("\"");
break;
}
}
if (filename is null || filename.length < 1) {
throw new HttpStatusException(
HttpStatus.BAD_REQUEST,
"Missing filename for multipart file."
);
}
return MultipartFile(
filename,
e.headers["Content-Type"],
cast(ubyte[]) e.content
);
}
/**
* Struct representing a user-provided time range with optional "from" and "to"
* timestamps. If both are empty, it is assumed that the user is requesting all
* data.
*/
struct TimeRange {
Optional!SysTime fromTime;
Optional!SysTime toTime;
}