Compare commits
7 Commits
0706dd5e6f
...
8727a4e038
Author | SHA1 | Date | |
---|---|---|---|
8727a4e038 | |||
714e7278fd | |||
b2c915f400 | |||
b44ea5c3e8 | |||
4b26bc884f | |||
fc8a73a9c4 | |||
49f3f48aa8 |
22
README.md
22
README.md
|
@ -2,15 +2,13 @@
|
|||
|
||||
[![Build Status](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/status.svg?ref=refs/heads/master)](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/)
|
||||
|
||||
This SDK currently supports all AWS services except EC2 and S3. These two
|
||||
services only support XML, and more work is needed to parse and integrate
|
||||
type hydration from the base parsing. S3 also requires some plumbing tweaks
|
||||
in the signature calculation. Examples of usage are in src/main.zig.
|
||||
This SDK currently supports all AWS services except services using the restXml
|
||||
protocol (4 services including S3). See TODO list below.
|
||||
|
||||
Current executable size for the demo is 953k (90k of which is the AWS PEM file)
|
||||
after compiling with -Drelease-safe and
|
||||
Current executable size for the demo is 1.6M (90k of which is the AWS PEM file,
|
||||
and approximately 600K for XML services) after compiling with -Drelease-safe and
|
||||
[stripping the executable after compilation](https://github.com/ziglang/zig/issues/351).
|
||||
This is for x86_linux. Tested targets:
|
||||
This is for x86_linux, and will vary based on services used. Tested targets:
|
||||
|
||||
* x86_64-linux
|
||||
* riscv64-linux
|
||||
|
@ -41,8 +39,7 @@ require passing in a client option to specify an different TLS root certificate
|
|||
(pass null to disable certificate verification).
|
||||
|
||||
The [old branch](https://github.com/elerch/aws-sdk-for-zig/tree/aws-crt) exists
|
||||
for posterity, and supports x86_64 linux. This branch is recommended moving
|
||||
forward.
|
||||
for posterity, and supports x86_64 linux. The old branch is deprecated.
|
||||
|
||||
## Limitations
|
||||
|
||||
|
@ -52,13 +49,8 @@ implemented.
|
|||
|
||||
TODO List:
|
||||
|
||||
* Complete integration of Xml responses with remaining code base
|
||||
* Implement [AWS restXml protocol](https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html).
|
||||
Includes S3. Total service count 4. This may be blocked due to the same issue as EC2.
|
||||
* Implement [AWS EC2 query protocol](https://awslabs.github.io/smithy/1.0/spec/aws/aws-ec2-query-protocol.html).
|
||||
Includes EC2. Total service count 1. This may be blocked on a compiler bug,
|
||||
though has not been tested with zig 0.9.0. More details and llvm ir log can be found in the
|
||||
[XML branch](https://git.lerch.org/lobo/aws-sdk-for-zig/src/branch/xml).
|
||||
Includes S3. Total service count 4.
|
||||
* Implement sigv4a signing
|
||||
* Implement jitter/exponential backoff
|
||||
* Implement timeouts and other TODO's in the code
|
||||
|
|
93
src/aws.zig
93
src/aws.zig
|
@ -5,7 +5,7 @@ const json = @import("json.zig");
|
|||
const url = @import("url.zig");
|
||||
const case = @import("case.zig");
|
||||
const servicemodel = @import("servicemodel.zig");
|
||||
// const xml_shaper = @import("xml_shaper.zig");
|
||||
const xml_shaper = @import("xml_shaper.zig");
|
||||
|
||||
const log = std.log.scoped(.aws);
|
||||
|
||||
|
@ -175,8 +175,6 @@ pub fn Request(comptime action: anytype) type {
|
|||
// handle lists and maps properly anyway yet, so we'll go for it and see
|
||||
// where it breaks. PRs and/or failing test cases appreciated.
|
||||
fn callQuery(request: ActionRequest, options: Options) !FullResponseType {
|
||||
if (Self.service_meta.aws_protocol == .ec2_query)
|
||||
@compileError("XML responses from EC2 blocked due to zig compiler bug scheduled to be fixed no earlier than 0.10.0");
|
||||
var buffer = std.ArrayList(u8).init(options.client.allocator);
|
||||
defer buffer.deinit();
|
||||
const writer = buffer.writer();
|
||||
|
@ -250,10 +248,9 @@ pub fn Request(comptime action: anytype) type {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Handle XML
|
||||
if (!isJson) return error.XmlUnimplemented;
|
||||
if (!isJson) return try xmlReturn(options, response);
|
||||
|
||||
const SResponse = if (Self.service_meta.aws_protocol != .query and Self.service_meta.aws_protocol != .ec2_query)
|
||||
const SResponse = if (Self.service_meta.aws_protocol != .query)
|
||||
action.Response
|
||||
else
|
||||
ServerResponse(action);
|
||||
|
@ -272,7 +269,7 @@ pub fn Request(comptime action: anytype) type {
|
|||
.response_metadata = .{
|
||||
.request_id = try requestIdFromHeaders(aws_request, response, options),
|
||||
},
|
||||
.parser_options = parser_options,
|
||||
.parser_options = .{ .json = parser_options },
|
||||
.raw_parsed = .{ .raw = .{} },
|
||||
};
|
||||
|
||||
|
@ -294,13 +291,25 @@ pub fn Request(comptime action: anytype) type {
|
|||
return e;
|
||||
};
|
||||
|
||||
if (Self.service_meta.aws_protocol != .query and Self.service_meta.aws_protocol != .ec2_query) {
|
||||
// TODO: Figure out this hack
|
||||
// the code setting the response about 10 lines down will trigger
|
||||
// an error because the first field may not be a struct when
|
||||
// XML processing is happening above, which we only know at runtime.
|
||||
//
|
||||
// We could simply force .ec2_query and .rest_xml above rather than
|
||||
// isJson, but it would be nice to automatically support json if
|
||||
// these services start returning that like we'd like them to.
|
||||
//
|
||||
// Otherwise, the compiler gets down here thinking this will be
|
||||
// processed. If it is, then we have a problem when the field name
|
||||
// may not be a struct.
|
||||
if (Self.service_meta.aws_protocol != .query or Self.service_meta.aws_protocol == .ec2_query) {
|
||||
return FullResponseType{
|
||||
.response = parsed_response,
|
||||
.response_metadata = .{
|
||||
.request_id = try requestIdFromHeaders(aws_request, response, options),
|
||||
},
|
||||
.parser_options = parser_options,
|
||||
.parser_options = .{ .json = parser_options },
|
||||
.raw_parsed = .{ .raw = parsed_response },
|
||||
};
|
||||
}
|
||||
|
@ -320,10 +329,53 @@ pub fn Request(comptime action: anytype) type {
|
|||
.response_metadata = .{
|
||||
.request_id = try options.client.allocator.dupe(u8, real_response.ResponseMetadata.RequestId),
|
||||
},
|
||||
.parser_options = parser_options,
|
||||
.parser_options = .{ .json = parser_options },
|
||||
.raw_parsed = .{ .server = parsed_response },
|
||||
};
|
||||
}
|
||||
|
||||
fn xmlReturn(options: Options, result: awshttp.HttpResult) !FullResponseType {
|
||||
// Server shape be all like:
|
||||
//
|
||||
// <?xml version="1.0" encoding="UTF-8"?>
|
||||
// <DescribeRegionsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
|
||||
// <requestId>0efe31c6-cad5-4882-b275-dfea478cf039</requestId>
|
||||
// <regionInfo>
|
||||
// <item>
|
||||
// <regionName>eu-north-1</regionName>
|
||||
// <regionEndpoint>ec2.eu-north-1.amazonaws.com</regionEndpoint>
|
||||
// <optInStatus>opt-in-not-required</optInStatus>
|
||||
// </item>
|
||||
// </regionInfo>
|
||||
// </DescribeRegionsResponse>
|
||||
//
|
||||
// While our stuff be like:
|
||||
//
|
||||
// struct {
|
||||
// regions: []struct {
|
||||
// region_name: []const u8,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Big thing is that requestid, which we'll need to fetch "manually"
|
||||
const xml_options = xml_shaper.ParseOptions{ .allocator = options.client.allocator };
|
||||
const parsed = try xml_shaper.parse(action.Response, result.body, xml_options);
|
||||
// This needs to get into FullResponseType somehow: defer parsed.deinit();
|
||||
const request_id = blk: {
|
||||
if (parsed.document.root.getCharData("requestId")) |elem|
|
||||
break :blk elem;
|
||||
return error.RequestIdNotFound;
|
||||
};
|
||||
|
||||
return FullResponseType{
|
||||
.response = parsed.parsed_value,
|
||||
.response_metadata = .{
|
||||
.request_id = try options.client.allocator.dupe(u8, request_id),
|
||||
},
|
||||
.parser_options = .{ .xml = xml_options },
|
||||
.raw_parsed = .{ .xml = parsed },
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -397,21 +449,32 @@ fn FullResponse(comptime action: anytype) type {
|
|||
response_metadata: struct {
|
||||
request_id: []u8,
|
||||
},
|
||||
parser_options: json.ParseOptions,
|
||||
parser_options: union(enum) {
|
||||
json: json.ParseOptions,
|
||||
xml: xml_shaper.ParseOptions,
|
||||
},
|
||||
raw_parsed: union(enum) {
|
||||
server: ServerResponse(action),
|
||||
raw: action.Response,
|
||||
xml: xml_shaper.Parsed(action.Response),
|
||||
},
|
||||
// raw_parsed: ServerResponse(request),
|
||||
|
||||
const Self = @This();
|
||||
pub fn deinit(self: Self) void {
|
||||
switch (self.raw_parsed) {
|
||||
.server => json.parseFree(ServerResponse(action), self.raw_parsed.server, self.parser_options),
|
||||
.raw => json.parseFree(action.Response, self.raw_parsed.raw, self.parser_options),
|
||||
// Server is json only (so far)
|
||||
.server => json.parseFree(ServerResponse(action), self.raw_parsed.server, self.parser_options.json),
|
||||
// Raw is json only (so far)
|
||||
.raw => json.parseFree(action.Response, self.raw_parsed.raw, self.parser_options.json),
|
||||
.xml => |xml| xml.deinit(),
|
||||
}
|
||||
|
||||
self.parser_options.allocator.?.free(self.response_metadata.request_id);
|
||||
var allocator: std.mem.Allocator = undefined;
|
||||
switch (self.parser_options) {
|
||||
.json => |j| allocator = j.allocator.?,
|
||||
.xml => |x| allocator = x.allocator.?,
|
||||
}
|
||||
allocator.free(self.response_metadata.request_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
260
src/date.zig
260
src/date.zig
|
@ -4,14 +4,17 @@
|
|||
|
||||
const std = @import("std");
|
||||
|
||||
const log = std.log.scoped(.date);
|
||||
|
||||
pub const DateTime = struct { day: u8, month: u8, year: u16, hour: u8, minute: u8, second: u8 };
|
||||
|
||||
const SECONDS_PER_DAY = 86400; //* 24* 60 * 60 */
|
||||
const DAYS_PER_YEAR = 365; //* Normal year (no leap year) */
|
||||
|
||||
pub fn timestampToDateTime(timestamp: i64) DateTime {
|
||||
|
||||
// aus https://de.wikipedia.org/wiki/Unixzeit
|
||||
const unixtime = @intCast(u64, timestamp);
|
||||
const SECONDS_PER_DAY = 86400; //* 24* 60 * 60 */
|
||||
const DAYS_PER_YEAR = 365; //* Normal year (no leap year) */
|
||||
const DAYS_IN_4_YEARS = 1461; //* 4*365 + 1 */
|
||||
const DAYS_IN_100_YEARS = 36524; //* 100*365 + 25 - 1 */
|
||||
const DAYS_IN_400_YEARS = 146097; //* 400*365 + 100 - 4 + 1 */
|
||||
|
@ -54,8 +57,239 @@ pub fn timestampToDateTime(timestamp: i64) DateTime {
|
|||
return DateTime{ .day = day, .month = month, .year = year, .hour = hours, .minute = minutes, .second = seconds };
|
||||
}
|
||||
|
||||
pub fn parseIso8601ToTimestamp(data: []const u8) !i64 {
|
||||
return try dateTimeToTimestamp(try parseIso8601ToDateTime(data));
|
||||
}
|
||||
|
||||
const IsoParsingState = enum { Start, Year, Month, Day, Hour, Minute, Second, Millisecond, End };
|
||||
/// Converts a string to a timestamp value. May not handle dates before the
|
||||
/// epoch
|
||||
pub fn parseIso8601ToDateTime(data: []const u8) !DateTime {
|
||||
// Basic format YYYYMMDDThhmmss
|
||||
if (data.len == "YYYYMMDDThhmmss".len and data[8] == 'T')
|
||||
return try parseIso8601BasicFormatToDateTime(data);
|
||||
|
||||
var start: usize = 0;
|
||||
var state = IsoParsingState.Start;
|
||||
// Anything not explicitly set by our string would be 0
|
||||
var rc = DateTime{ .year = 0, .month = 0, .day = 0, .hour = 0, .minute = 0, .second = 0 };
|
||||
var zulu_time = false;
|
||||
for (data) |ch, i| {
|
||||
_ = i;
|
||||
switch (ch) {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
|
||||
if (state == .Start) state = .Year;
|
||||
},
|
||||
'?', '~', '%' => {
|
||||
// These characters all specify the type of time (approximate, etc)
|
||||
// and we will ignore
|
||||
},
|
||||
'.', '-', ':', 'T' => {
|
||||
// State transition
|
||||
|
||||
// We're going to coerce and this might not go well, but we
|
||||
// want the compiler to create checks, so we'll turn on
|
||||
// runtime safety for this block, forcing checks in ReleaseSafe
|
||||
// ReleaseFast modes.
|
||||
const next_state = try endIsoState(state, &rc, data[start..i]);
|
||||
state = next_state;
|
||||
start = i + 1;
|
||||
},
|
||||
'Z' => zulu_time = true,
|
||||
else => {
|
||||
log.err("Invalid character: {c}", .{ch});
|
||||
return error.InvalidCharacter;
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!zulu_time) return error.LocalTimeNotSupported;
|
||||
// We know we have a Z at the end of this, so let's grab the last bit
|
||||
// of the string, minus the 'Z', and fly, eagles, fly!
|
||||
_ = try endIsoState(state, &rc, data[start .. data.len - 1]);
|
||||
return rc;
|
||||
}
|
||||
|
||||
fn parseIso8601BasicFormatToDateTime(data: []const u8) !DateTime {
|
||||
return DateTime{
|
||||
.year = try std.fmt.parseUnsigned(u16, data[0..4], 10),
|
||||
.month = try std.fmt.parseUnsigned(u8, data[4..6], 10),
|
||||
.day = try std.fmt.parseUnsigned(u8, data[6..8], 10),
|
||||
.hour = try std.fmt.parseUnsigned(u8, data[9..11], 10),
|
||||
.minute = try std.fmt.parseUnsigned(u8, data[11..13], 10),
|
||||
.second = try std.fmt.parseUnsigned(u8, data[13..15], 10),
|
||||
};
|
||||
}
|
||||
|
||||
fn endIsoState(current_state: IsoParsingState, date: *DateTime, prev_data: []const u8) !IsoParsingState {
|
||||
var next_state: IsoParsingState = undefined;
|
||||
log.debug("endIsoState. Current state '{s}', data: {s}", .{ current_state, prev_data });
|
||||
|
||||
// Using two switches is slightly less efficient, but more readable
|
||||
switch (current_state) {
|
||||
.Start, .End => return error.IllegalStateTransition,
|
||||
.Year => next_state = .Month,
|
||||
.Month => next_state = .Day,
|
||||
.Day => next_state = .Hour,
|
||||
.Hour => next_state = .Minute,
|
||||
.Minute => next_state = .Second,
|
||||
.Second => next_state = .Millisecond,
|
||||
.Millisecond => next_state = .End,
|
||||
}
|
||||
|
||||
// TODO: This won't handle signed, which Iso supports. For now, let's fail
|
||||
// explictly
|
||||
switch (current_state) {
|
||||
.Year => date.year = try std.fmt.parseUnsigned(u16, prev_data, 10),
|
||||
.Month => date.month = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
||||
.Day => date.day = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
||||
.Hour => date.hour = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
||||
.Minute => date.minute = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
||||
.Second => date.second = try std.fmt.parseUnsigned(u8, prev_data, 10),
|
||||
.Millisecond => {}, // We'll throw that away - our granularity is 1 second
|
||||
.Start, .End => return error.InvalidState,
|
||||
}
|
||||
return next_state;
|
||||
}
|
||||
fn dateTimeToTimestamp(datetime: DateTime) !i64 {
|
||||
const epoch = DateTime{
|
||||
.year = 1970,
|
||||
.month = 1,
|
||||
.day = 1,
|
||||
.hour = 0,
|
||||
.minute = 0,
|
||||
.second = 0,
|
||||
};
|
||||
return secondsBetween(epoch, datetime);
|
||||
}
|
||||
|
||||
const DateTimeToTimestampError = error{
|
||||
DateTimeOutOfRange,
|
||||
};
|
||||
|
||||
fn secondsBetween(start: DateTime, end: DateTime) DateTimeToTimestampError!i64 {
|
||||
try validateDatetime(start);
|
||||
try validateDatetime(end);
|
||||
if (end.year < start.year) return -1 * try secondsBetween(end, start);
|
||||
if (start.month != 1 or
|
||||
start.day != 1 or
|
||||
start.hour != 0 or
|
||||
start.minute != 0 or
|
||||
start.second != 0)
|
||||
{
|
||||
const seconds_into_start_year = secondsFromBeginningOfYear(
|
||||
start.year,
|
||||
start.month,
|
||||
start.day,
|
||||
start.hour,
|
||||
start.minute,
|
||||
start.second,
|
||||
);
|
||||
const new_start = DateTime{
|
||||
.year = start.year,
|
||||
.month = 1,
|
||||
.day = 1,
|
||||
.hour = 0,
|
||||
.minute = 0,
|
||||
.second = 0,
|
||||
};
|
||||
return (try secondsBetween(new_start, end)) - seconds_into_start_year;
|
||||
}
|
||||
const leap_years_between = leapYearsBetween(start.year, end.year);
|
||||
var add_days: u1 = 0;
|
||||
const years_diff = end.year - start.year;
|
||||
log.debug("Years from epoch: {d}, Leap years: {d}", .{ years_diff, leap_years_between });
|
||||
var days_diff: i32 = (years_diff * DAYS_PER_YEAR) + leap_years_between + add_days;
|
||||
log.debug("Days with leap year, without month: {d}", .{days_diff});
|
||||
|
||||
const seconds_into_year = secondsFromBeginningOfYear(
|
||||
end.year,
|
||||
end.month,
|
||||
end.day,
|
||||
end.hour,
|
||||
end.minute,
|
||||
end.second,
|
||||
);
|
||||
return (days_diff * SECONDS_PER_DAY) + @as(i64, seconds_into_year);
|
||||
}
|
||||
|
||||
fn validateDatetime(dt: DateTime) !void {
|
||||
if (dt.month > 12 or
|
||||
dt.day > 31 or
|
||||
dt.hour >= 24 or
|
||||
dt.minute >= 60 or
|
||||
dt.second >= 60) return error.DateTimeOutOfRange;
|
||||
}
|
||||
|
||||
fn secondsFromBeginningOfYear(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) u32 {
|
||||
const current_year_is_leap_year = isLeapYear(year);
|
||||
const leap_year_days_per_month: [12]u5 = .{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
const normal_days_per_month: [12]u5 = .{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
const days_per_month = if (current_year_is_leap_year) leap_year_days_per_month else normal_days_per_month;
|
||||
var current_month: usize = 1;
|
||||
var end_month = month;
|
||||
var days_diff: u32 = 0;
|
||||
while (current_month != end_month) {
|
||||
days_diff += days_per_month[current_month - 1]; // months are 1-based vs array is 0-based
|
||||
current_month += 1;
|
||||
}
|
||||
log.debug("Days with month, without day: {d}. Day of month {d}, will add {d} days", .{
|
||||
days_diff,
|
||||
day,
|
||||
day - 1,
|
||||
});
|
||||
// We need -1 because we're not actually including the ending day (that's up to hour/minute)
|
||||
// In other words, days in the month are 1-based, while hours/minutes are zero based
|
||||
days_diff += day - 1;
|
||||
log.debug("Total days diff: {d}", .{days_diff});
|
||||
var seconds_diff: u32 = days_diff * SECONDS_PER_DAY;
|
||||
|
||||
// From here out, we want to get everything into seconds
|
||||
seconds_diff += @as(u32, hour) * 60 * 60;
|
||||
seconds_diff += @as(u32, minute) * 60;
|
||||
seconds_diff += @as(u32, second);
|
||||
|
||||
return seconds_diff;
|
||||
}
|
||||
fn isLeapYear(year: u16) bool {
|
||||
if (year % 4 != 0) return false;
|
||||
if (year % 400 == 0) return true;
|
||||
if (year % 100 == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn leapYearsBetween(start_year_inclusive: u16, end_year_exclusive: u16) u16 {
|
||||
const start = std.math.min(start_year_inclusive, end_year_exclusive);
|
||||
const end = std.math.max(start_year_inclusive, end_year_exclusive);
|
||||
var current = start;
|
||||
log.debug("Leap years starting from {d}, ending at {d}", .{ start, end });
|
||||
while (current % 4 != 0 and current < end) {
|
||||
current += 1;
|
||||
}
|
||||
if (current == end) return 0; // No leap years here. E.g. 1971-1973
|
||||
// We're on a potential leap year, and now we can step by 4
|
||||
var rc: u16 = 0;
|
||||
while (current < end) {
|
||||
if (current % 4 == 0) {
|
||||
if (current % 100 != 0) {
|
||||
log.debug("Year {d} is leap year", .{current});
|
||||
rc += 1;
|
||||
current += 4;
|
||||
continue;
|
||||
}
|
||||
// We're on a century, which is normally not a leap year, unless
|
||||
// it's divisible by 400
|
||||
if (current % 400 == 0) {
|
||||
log.debug("Year {d} is leap year", .{current});
|
||||
rc += 1;
|
||||
}
|
||||
}
|
||||
current += 4;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
fn printDateTime(dt: DateTime) void {
|
||||
std.log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
|
||||
log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
|
||||
dt.year,
|
||||
dt.month,
|
||||
dt.day,
|
||||
|
@ -69,9 +303,7 @@ pub fn printNowUtc() void {
|
|||
printDateTime(timestampToDateTime(std.time.timestamp()));
|
||||
}
|
||||
|
||||
test "GMT and localtime" {
|
||||
std.testing.log_level = .debug;
|
||||
std.log.debug("\n", .{});
|
||||
test "Convert timestamp to datetime" {
|
||||
printDateTime(timestampToDateTime(std.time.timestamp()));
|
||||
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, timestampToDateTime(1598607147));
|
||||
|
||||
|
@ -79,3 +311,19 @@ test "GMT and localtime" {
|
|||
// Get time for date: https://wtools.io/convert-date-time-to-unix-time
|
||||
try std.testing.expectEqual(DateTime{ .year = 2015, .month = 08, .day = 30, .hour = 12, .minute = 36, .second = 00 }, timestampToDateTime(1440938160));
|
||||
}
|
||||
|
||||
test "Convert datetime to timestamp" {
|
||||
try std.testing.expectEqual(@as(i64, 1598607147), try dateTimeToTimestamp(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }));
|
||||
try std.testing.expectEqual(@as(i64, 1604207167), try dateTimeToTimestamp(DateTime{ .year = 2020, .month = 11, .day = 1, .hour = 5, .minute = 6, .second = 7 }));
|
||||
try std.testing.expectEqual(@as(i64, 1440938160), try dateTimeToTimestamp(DateTime{ .year = 2015, .month = 08, .day = 30, .hour = 12, .minute = 36, .second = 00 }));
|
||||
}
|
||||
|
||||
test "Convert ISO8601 string to timestamp" {
|
||||
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, try parseIso8601ToDateTime("20200828T093227"));
|
||||
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, try parseIso8601ToDateTime("2020-08-28T9:32:27Z"));
|
||||
try std.testing.expectEqual(DateTime{ .year = 2020, .month = 11, .day = 1, .hour = 5, .minute = 6, .second = 7 }, try parseIso8601ToDateTime("2020-11-01T5:06:7Z"));
|
||||
try std.testing.expectEqual(DateTime{ .year = 2015, .month = 08, .day = 30, .hour = 12, .minute = 36, .second = 00 }, try parseIso8601ToDateTime("2015-08-30T12:36:00.000Z"));
|
||||
}
|
||||
test "Convert datetime to timestamp before 1970" {
|
||||
try std.testing.expectEqual(@as(i64, -449392815), try dateTimeToTimestamp(DateTime{ .year = 1955, .month = 10, .day = 05, .hour = 16, .minute = 39, .second = 45 }));
|
||||
}
|
||||
|
|
24
src/main.zig
24
src/main.zig
|
@ -14,6 +14,15 @@ pub fn log(
|
|||
// Ignore aws_signing messages
|
||||
if (verbose < 2 and scope == .aws_signing and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
|
||||
return;
|
||||
// Ignore aws_credentials messages
|
||||
if (verbose < 2 and scope == .aws_credentials and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
|
||||
return;
|
||||
// Ignore xml_shaper messages
|
||||
if (verbose < 2 and scope == .xml_shaper and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
|
||||
return;
|
||||
// Ignore date messages
|
||||
if (verbose < 2 and scope == .date and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
|
||||
return;
|
||||
// Ignore awshttp messages
|
||||
if (verbose < 1 and scope == .awshttp and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
|
||||
return;
|
||||
|
@ -169,18 +178,17 @@ pub fn main() anyerror!void {
|
|||
std.log.err("no functions to work with", .{});
|
||||
}
|
||||
},
|
||||
// TODO: This test fails with broken LLVM module
|
||||
.ec2_query_no_input => {
|
||||
std.log.err("EC2 Test disabled due to compiler bug", .{});
|
||||
// Describe regions is a simpler request and easier to debug
|
||||
// const instances = try client.call(services.ec2.describe_regions.Request{}, options);
|
||||
// defer instances.deinit();
|
||||
// std.log.info("region count: {d}", .{instances.response.regions.?.len});
|
||||
const result = try client.call(services.ec2.describe_regions.Request{}, options);
|
||||
defer result.deinit();
|
||||
std.log.info("request id: {s}", .{result.response_metadata.request_id});
|
||||
std.log.info("region count: {d}", .{result.response.regions.?.len});
|
||||
|
||||
// Describe instances is more interesting
|
||||
// const instances = try client.call(services.ec2.describe_instances.Request{}, options);
|
||||
// defer instances.deinit();
|
||||
// std.log.info("reservation count: {d}", .{instances.response.reservations.len});
|
||||
const instances = try client.call(services.ec2.describe_instances.Request{}, options);
|
||||
defer instances.deinit();
|
||||
std.log.info("reservation count: {d}", .{instances.response.reservations.?.len});
|
||||
},
|
||||
}
|
||||
std.log.info("===== End Test: {s} =====\n", .{@tagName(t)});
|
||||
|
|
|
@ -45,7 +45,7 @@ pub const Element = struct {
|
|||
}
|
||||
|
||||
pub fn getCharData(self: *Element, child_tag: []const u8) ?[]const u8 {
|
||||
const child = self.findChildByTag(child_tag) orelse return null;
|
||||
const child = (self.findChildByTag(child_tag) catch return null) orelse return null;
|
||||
if (child.children.items.len != 1) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,81 +1,71 @@
|
|||
const std = @import("std");
|
||||
const xml = @import("xml.zig");
|
||||
const date = @import("date.zig");
|
||||
|
||||
const log = std.log.scoped(.xml_shaper);
|
||||
|
||||
fn Parsed(comptime T: type) type {
|
||||
pub fn Parsed(comptime T: type) type {
|
||||
return struct {
|
||||
allocator: std.mem.Allocator,
|
||||
// Forcing an arean allocator isn't my favorite choice here, but
|
||||
// is the simplest way to handle deallocation in the event of
|
||||
// an error
|
||||
allocator: std.heap.ArenaAllocator,
|
||||
parsed_value: T,
|
||||
document: xml.Document,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, parsedObj: T) Self {
|
||||
pub fn init(allocator: std.heap.ArenaAllocator, parsedObj: T, document: xml.Document) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.parsed_value = parsedObj,
|
||||
.document = document,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
deinitObject(self.allocator, self.parsed_value);
|
||||
self.allocator.deinit();
|
||||
// deinitObject(self.allocator, self.parsed_value);
|
||||
// self.document.deinit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
|
||||
switch (@typeInfo(@TypeOf(obj))) {
|
||||
.Optional => if (obj) |o| deinitObject(allocator, o),
|
||||
.Union => |union_info| {
|
||||
inline for (union_info.fields) |field| {
|
||||
std.debug.print("{s}", field); // need to find active field and deinit it
|
||||
}
|
||||
},
|
||||
.Struct => |struct_info| {
|
||||
inline for (struct_info.fields) |field| {
|
||||
deinitObject(allocator, @field(obj, field.name));
|
||||
}
|
||||
},
|
||||
.Array => {}, // Not implemented below
|
||||
.Pointer => |ptr_info| {
|
||||
switch (ptr_info.size) {
|
||||
.One => {
|
||||
deinitObject(allocator, obj.*);
|
||||
allocator.free(obj);
|
||||
},
|
||||
.Many => {},
|
||||
.C => {},
|
||||
.Slice => {
|
||||
for (obj) |child|
|
||||
deinitObject(allocator, child);
|
||||
allocator.free(obj);
|
||||
},
|
||||
}
|
||||
},
|
||||
//.Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum, .Opaque => {}, // no allocations here
|
||||
else => {},
|
||||
// This is dead code and can be removed with the move to ArenaAllocator
|
||||
fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
|
||||
switch (@typeInfo(@TypeOf(obj))) {
|
||||
.Optional => if (obj) |o| deinitObject(allocator, o),
|
||||
.Union => |union_info| {
|
||||
inline for (union_info.fields) |field| {
|
||||
std.debug.print("{s}", field); // need to find active field and deinit it
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
.Struct => |struct_info| {
|
||||
inline for (struct_info.fields) |field| {
|
||||
deinitObject(allocator, @field(obj, field.name));
|
||||
}
|
||||
},
|
||||
.Array => {}, // Not implemented below
|
||||
.Pointer => |ptr_info| {
|
||||
switch (ptr_info.size) {
|
||||
.One => {
|
||||
deinitObject(allocator, obj.*);
|
||||
allocator.free(obj);
|
||||
},
|
||||
.Many => {},
|
||||
.C => {},
|
||||
.Slice => {
|
||||
for (obj) |child|
|
||||
deinitObject(allocator, child);
|
||||
allocator.free(obj);
|
||||
},
|
||||
}
|
||||
},
|
||||
//.Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum, .Opaque => {}, // no allocations here
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Parser(comptime T: type) type {
|
||||
return struct {
|
||||
ParseType: type = T,
|
||||
ReturnType: type = Parsed(T),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn parse(source: []const u8, options: ParseOptions) !Parsed(T) {
|
||||
if (options.allocator == null)
|
||||
return error.AllocatorRequired; // we are only leaving it be null for compatibility with json
|
||||
const allocator = options.allocator.?;
|
||||
const parse_allocator = std.heap.ArenaAllocator.init(allocator);
|
||||
const parsed = try xml.parse(allocator, source);
|
||||
defer parsed.deinit();
|
||||
defer parse_allocator.deinit();
|
||||
return Parsed(T).init(allocator, try parseInternal(T, parsed.root, options));
|
||||
}
|
||||
};
|
||||
}
|
||||
// should we just use json parse options?
|
||||
pub const ParseOptions = struct {
|
||||
allocator: ?std.mem.Allocator = null,
|
||||
|
@ -86,11 +76,17 @@ pub fn parse(comptime T: type, source: []const u8, options: ParseOptions) !Parse
|
|||
if (options.allocator == null)
|
||||
return error.AllocatorRequired; // we are only leaving it be null for compatibility with json
|
||||
const allocator = options.allocator.?;
|
||||
var parse_allocator = std.heap.ArenaAllocator.init(allocator);
|
||||
const parsed = try xml.parse(parse_allocator.allocator(), source);
|
||||
// defer parsed.deinit(); // Let the arena allocator whack it all
|
||||
defer parse_allocator.deinit();
|
||||
return Parsed(T).init(allocator, try parseInternal(T, parsed.root, options));
|
||||
var arena_allocator = std.heap.ArenaAllocator.init(allocator);
|
||||
const aa = arena_allocator.allocator();
|
||||
errdefer arena_allocator.deinit();
|
||||
const parsed = try xml.parse(aa, source);
|
||||
errdefer parsed.deinit();
|
||||
const opts = ParseOptions{
|
||||
.allocator = aa,
|
||||
.match_predicate = options.match_predicate,
|
||||
};
|
||||
|
||||
return Parsed(T).init(arena_allocator, try parseInternal(T, parsed.root, opts), parsed);
|
||||
}
|
||||
|
||||
fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions) !T {
|
||||
|
@ -103,10 +99,47 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
|
|||
return error.UnexpectedToken;
|
||||
},
|
||||
.Float, .ComptimeFloat => {
|
||||
return try std.fmt.parseFloat(T, element.children.items[0].CharData);
|
||||
return std.fmt.parseFloat(T, element.children.items[0].CharData) catch |e| {
|
||||
if (log_parse_traces) {
|
||||
std.log.err(
|
||||
"Could not parse '{s}' as float in element '{s}': {s}",
|
||||
.{
|
||||
element.children.items[0].CharData,
|
||||
element.tag,
|
||||
e,
|
||||
},
|
||||
);
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
};
|
||||
},
|
||||
.Int, .ComptimeInt => {
|
||||
return try std.fmt.parseInt(T, element.children.items[0].CharData, 10);
|
||||
// 2021-10-05T16:39:45.000Z
|
||||
return std.fmt.parseInt(T, element.children.items[0].CharData, 10) catch |e| {
|
||||
if (element.children.items[0].CharData[element.children.items[0].CharData.len - 1] == 'Z') {
|
||||
// We have an iso8601 in an integer field (we think)
|
||||
// Try to coerce this into our type
|
||||
const timestamp = try date.parseIso8601ToTimestamp(element.children.items[0].CharData);
|
||||
return try std.math.cast(T, timestamp);
|
||||
}
|
||||
if (log_parse_traces) {
|
||||
std.log.err(
|
||||
"Could not parse '{s}' as integer in element '{s}': {s}",
|
||||
.{
|
||||
element.children.items[0].CharData,
|
||||
element.tag,
|
||||
e,
|
||||
},
|
||||
);
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
};
|
||||
},
|
||||
.Optional => |optional_info| {
|
||||
if (element.children.items.len == 0) {
|
||||
|
@ -200,13 +233,23 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
|
|||
// }
|
||||
// } else {
|
||||
log.debug("Found child element {s}", .{child.tag});
|
||||
// TODO: how do we errdefer this?
|
||||
@field(r, field.name) = try parseInternal(field.field_type, child, options);
|
||||
fields_seen[i] = true;
|
||||
fields_set = fields_set + 1;
|
||||
found_value = true;
|
||||
}
|
||||
if (@typeInfo(field.field_type) == .Optional and !found_value) {
|
||||
// @compileLog("Optional: Field name ", field.name, ", type ", field.field_type);
|
||||
@field(r, field.name) = null;
|
||||
fields_set = fields_set + 1;
|
||||
found_value = true;
|
||||
}
|
||||
// Using this else clause breaks zig, so we'll use a boolean instead
|
||||
if (!found_value) return error.NoValueForField;
|
||||
if (!found_value) {
|
||||
log.err("Could not find a value for field {s}. Looking for {s} in element {s}", .{ field.name, name, element.tag });
|
||||
return error.NoValueForField;
|
||||
}
|
||||
// } else {
|
||||
// return error.NoValueForField;
|
||||
// }
|
||||
|
@ -416,6 +459,21 @@ test "can parse an optional boolean type" {
|
|||
try testing.expectEqual(@as(?bool, true), parsed_data.parsed_value.foo_bar);
|
||||
}
|
||||
|
||||
test "can coerce 8601 date to integer" {
|
||||
const allocator = std.testing.allocator;
|
||||
const data =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
||||
\\ <fooBar>2021-10-05T16:39:45.000Z</fooBar>
|
||||
\\</Example>
|
||||
;
|
||||
const ExampleDoesNotMatter = struct {
|
||||
foo_bar: ?i64 = null,
|
||||
};
|
||||
const parsed_data = try parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate = fuzzyEqual });
|
||||
defer parsed_data.deinit();
|
||||
try testing.expectEqual(@as(i64, 1633451985), parsed_data.parsed_value.foo_bar.?);
|
||||
}
|
||||
// This is the simplest test so far that breaks zig
|
||||
test "can parse a boolean type (two fields)" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
@ -434,6 +492,28 @@ test "can parse a boolean type (two fields)" {
|
|||
defer parsed_data.deinit();
|
||||
try testing.expectEqual(@as(bool, true), parsed_data.parsed_value.foo_bar);
|
||||
}
|
||||
var log_parse_traces = true;
|
||||
test "can error without leaking memory" {
|
||||
const allocator = std.testing.allocator;
|
||||
const data =
|
||||
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||
\\<Example xmlns="http://example.example.com/doc/2016-11-15/">
|
||||
\\ <fooBar>true</fooBar>
|
||||
\\ <fooBaz>12.345</fooBaz>
|
||||
\\</Example>
|
||||
;
|
||||
const ExampleDoesNotMatter = struct {
|
||||
foo_bar: bool,
|
||||
foo_baz: u64,
|
||||
};
|
||||
log_parse_traces = false;
|
||||
defer log_parse_traces = true;
|
||||
try std.testing.expectError(
|
||||
error.InvalidCharacter,
|
||||
parse(ExampleDoesNotMatter, data, .{ .allocator = allocator, .match_predicate = fuzzyEqual }),
|
||||
);
|
||||
}
|
||||
|
||||
test "can parse a nested type" {
|
||||
const allocator = std.testing.allocator;
|
||||
const data =
|
||||
|
|
Loading…
Reference in New Issue
Block a user