Compare commits
	
		
			No commits in common. "8727a4e03810f0ccbbd0b6b3020efe5eac337a0d" and "0706dd5e6ff4e09c76472da621165b715493f98a" have entirely different histories.
		
	
	
		
			8727a4e038
			...
			0706dd5e6f
		
	
		
					 6 changed files with 109 additions and 500 deletions
				
			
		
							
								
								
									
										22
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
					@ -2,13 +2,15 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/)
 | 
					[](https://drone.lerch.org/api/badges/lobo/aws-sdk-for-zig/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This SDK currently supports all AWS services except services using the restXml
 | 
					This SDK currently supports all AWS services except EC2 and S3. These two
 | 
				
			||||||
protocol (4 services including S3). See TODO list below.
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Current executable size for the demo is 1.6M (90k of which is the AWS PEM file,
 | 
					Current executable size for the demo is 953k (90k of which is the AWS PEM file)
 | 
				
			||||||
and approximately 600K for XML services) after compiling with -Drelease-safe and
 | 
					after compiling with -Drelease-safe and
 | 
				
			||||||
[stripping the executable after compilation](https://github.com/ziglang/zig/issues/351).
 | 
					[stripping the executable after compilation](https://github.com/ziglang/zig/issues/351).
 | 
				
			||||||
This is for x86_linux, and will vary based on services used. Tested targets:
 | 
					This is for x86_linux. Tested targets:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* x86_64-linux
 | 
					* x86_64-linux
 | 
				
			||||||
* riscv64-linux
 | 
					* riscv64-linux
 | 
				
			||||||
| 
						 | 
					@ -39,7 +41,8 @@ require passing in a client option to specify an different TLS root certificate
 | 
				
			||||||
(pass null to disable certificate verification).
 | 
					(pass null to disable certificate verification).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The [old branch](https://github.com/elerch/aws-sdk-for-zig/tree/aws-crt) exists
 | 
					The [old branch](https://github.com/elerch/aws-sdk-for-zig/tree/aws-crt) exists
 | 
				
			||||||
for posterity, and supports x86_64 linux. The old branch is deprecated.
 | 
					for posterity, and supports x86_64 linux. This branch is recommended moving
 | 
				
			||||||
 | 
					forward.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Limitations
 | 
					## Limitations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,8 +52,13 @@ implemented.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TODO List:
 | 
					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).
 | 
					* Implement [AWS restXml protocol](https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html).
 | 
				
			||||||
  Includes S3. Total service count 4.
 | 
					  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).
 | 
				
			||||||
* Implement sigv4a signing
 | 
					* Implement sigv4a signing
 | 
				
			||||||
* Implement jitter/exponential backoff
 | 
					* Implement jitter/exponential backoff
 | 
				
			||||||
* Implement timeouts and other TODO's in the code
 | 
					* 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 url = @import("url.zig");
 | 
				
			||||||
const case = @import("case.zig");
 | 
					const case = @import("case.zig");
 | 
				
			||||||
const servicemodel = @import("servicemodel.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);
 | 
					const log = std.log.scoped(.aws);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -175,6 +175,8 @@ pub fn Request(comptime action: anytype) type {
 | 
				
			||||||
        // handle lists and maps properly anyway yet, so we'll go for it and see
 | 
					        // 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.
 | 
					        // where it breaks. PRs and/or failing test cases appreciated.
 | 
				
			||||||
        fn callQuery(request: ActionRequest, options: Options) !FullResponseType {
 | 
					        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);
 | 
					            var buffer = std.ArrayList(u8).init(options.client.allocator);
 | 
				
			||||||
            defer buffer.deinit();
 | 
					            defer buffer.deinit();
 | 
				
			||||||
            const writer = buffer.writer();
 | 
					            const writer = buffer.writer();
 | 
				
			||||||
| 
						 | 
					@ -248,9 +250,10 @@ pub fn Request(comptime action: anytype) type {
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!isJson) return try xmlReturn(options, response);
 | 
					            // TODO: Handle XML
 | 
				
			||||||
 | 
					            if (!isJson) return error.XmlUnimplemented;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const SResponse = if (Self.service_meta.aws_protocol != .query)
 | 
					            const SResponse = if (Self.service_meta.aws_protocol != .query and Self.service_meta.aws_protocol != .ec2_query)
 | 
				
			||||||
                action.Response
 | 
					                action.Response
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
                ServerResponse(action);
 | 
					                ServerResponse(action);
 | 
				
			||||||
| 
						 | 
					@ -269,7 +272,7 @@ pub fn Request(comptime action: anytype) type {
 | 
				
			||||||
                    .response_metadata = .{
 | 
					                    .response_metadata = .{
 | 
				
			||||||
                        .request_id = try requestIdFromHeaders(aws_request, response, options),
 | 
					                        .request_id = try requestIdFromHeaders(aws_request, response, options),
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    .parser_options = .{ .json = parser_options },
 | 
					                    .parser_options = parser_options,
 | 
				
			||||||
                    .raw_parsed = .{ .raw = .{} },
 | 
					                    .raw_parsed = .{ .raw = .{} },
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -291,25 +294,13 @@ pub fn Request(comptime action: anytype) type {
 | 
				
			||||||
                return e;
 | 
					                return e;
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // TODO: Figure out this hack
 | 
					            if (Self.service_meta.aws_protocol != .query and Self.service_meta.aws_protocol != .ec2_query) {
 | 
				
			||||||
            // 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{
 | 
					                return FullResponseType{
 | 
				
			||||||
                    .response = parsed_response,
 | 
					                    .response = parsed_response,
 | 
				
			||||||
                    .response_metadata = .{
 | 
					                    .response_metadata = .{
 | 
				
			||||||
                        .request_id = try requestIdFromHeaders(aws_request, response, options),
 | 
					                        .request_id = try requestIdFromHeaders(aws_request, response, options),
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    .parser_options = .{ .json = parser_options },
 | 
					                    .parser_options = parser_options,
 | 
				
			||||||
                    .raw_parsed = .{ .raw = parsed_response },
 | 
					                    .raw_parsed = .{ .raw = parsed_response },
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -329,53 +320,10 @@ pub fn Request(comptime action: anytype) type {
 | 
				
			||||||
                .response_metadata = .{
 | 
					                .response_metadata = .{
 | 
				
			||||||
                    .request_id = try options.client.allocator.dupe(u8, real_response.ResponseMetadata.RequestId),
 | 
					                    .request_id = try options.client.allocator.dupe(u8, real_response.ResponseMetadata.RequestId),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                .parser_options = .{ .json = parser_options },
 | 
					                .parser_options = parser_options,
 | 
				
			||||||
                .raw_parsed = .{ .server = parsed_response },
 | 
					                .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 },
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -449,32 +397,21 @@ fn FullResponse(comptime action: anytype) type {
 | 
				
			||||||
        response_metadata: struct {
 | 
					        response_metadata: struct {
 | 
				
			||||||
            request_id: []u8,
 | 
					            request_id: []u8,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        parser_options: union(enum) {
 | 
					        parser_options: json.ParseOptions,
 | 
				
			||||||
            json: json.ParseOptions,
 | 
					 | 
				
			||||||
            xml: xml_shaper.ParseOptions,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        raw_parsed: union(enum) {
 | 
					        raw_parsed: union(enum) {
 | 
				
			||||||
            server: ServerResponse(action),
 | 
					            server: ServerResponse(action),
 | 
				
			||||||
            raw: action.Response,
 | 
					            raw: action.Response,
 | 
				
			||||||
            xml: xml_shaper.Parsed(action.Response),
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        // raw_parsed: ServerResponse(request),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const Self = @This();
 | 
					        const Self = @This();
 | 
				
			||||||
        pub fn deinit(self: Self) void {
 | 
					        pub fn deinit(self: Self) void {
 | 
				
			||||||
            switch (self.raw_parsed) {
 | 
					            switch (self.raw_parsed) {
 | 
				
			||||||
                // Server is json only (so far)
 | 
					                .server => json.parseFree(ServerResponse(action), self.raw_parsed.server, self.parser_options),
 | 
				
			||||||
                .server => json.parseFree(ServerResponse(action), self.raw_parsed.server, self.parser_options.json),
 | 
					                .raw => json.parseFree(action.Response, self.raw_parsed.raw, self.parser_options),
 | 
				
			||||||
                // Raw is json only (so far)
 | 
					 | 
				
			||||||
                .raw => json.parseFree(action.Response, self.raw_parsed.raw, self.parser_options.json),
 | 
					 | 
				
			||||||
                .xml => |xml| xml.deinit(),
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var allocator: std.mem.Allocator = undefined;
 | 
					            self.parser_options.allocator.?.free(self.response_metadata.request_id);
 | 
				
			||||||
            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,17 +4,14 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const std = @import("std");
 | 
					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 };
 | 
					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 {
 | 
					pub fn timestampToDateTime(timestamp: i64) DateTime {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // aus https://de.wikipedia.org/wiki/Unixzeit
 | 
					    // aus https://de.wikipedia.org/wiki/Unixzeit
 | 
				
			||||||
    const unixtime = @intCast(u64, timestamp);
 | 
					    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_4_YEARS = 1461; //*   4*365 +   1 */
 | 
				
			||||||
    const DAYS_IN_100_YEARS = 36524; //* 100*365 +  25 - 1 */
 | 
					    const DAYS_IN_100_YEARS = 36524; //* 100*365 +  25 - 1 */
 | 
				
			||||||
    const DAYS_IN_400_YEARS = 146097; //* 400*365 + 100 - 4 + 1 */
 | 
					    const DAYS_IN_400_YEARS = 146097; //* 400*365 + 100 - 4 + 1 */
 | 
				
			||||||
| 
						 | 
					@ -57,239 +54,8 @@ pub fn timestampToDateTime(timestamp: i64) DateTime {
 | 
				
			||||||
    return DateTime{ .day = day, .month = month, .year = year, .hour = hours, .minute = minutes, .second = seconds };
 | 
					    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 {
 | 
					fn printDateTime(dt: DateTime) void {
 | 
				
			||||||
    log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
 | 
					    std.log.debug("{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0<2}Z", .{
 | 
				
			||||||
        dt.year,
 | 
					        dt.year,
 | 
				
			||||||
        dt.month,
 | 
					        dt.month,
 | 
				
			||||||
        dt.day,
 | 
					        dt.day,
 | 
				
			||||||
| 
						 | 
					@ -303,7 +69,9 @@ pub fn printNowUtc() void {
 | 
				
			||||||
    printDateTime(timestampToDateTime(std.time.timestamp()));
 | 
					    printDateTime(timestampToDateTime(std.time.timestamp()));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test "Convert timestamp to datetime" {
 | 
					test "GMT and localtime" {
 | 
				
			||||||
 | 
					    std.testing.log_level = .debug;
 | 
				
			||||||
 | 
					    std.log.debug("\n", .{});
 | 
				
			||||||
    printDateTime(timestampToDateTime(std.time.timestamp()));
 | 
					    printDateTime(timestampToDateTime(std.time.timestamp()));
 | 
				
			||||||
    try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, timestampToDateTime(1598607147));
 | 
					    try std.testing.expectEqual(DateTime{ .year = 2020, .month = 8, .day = 28, .hour = 9, .minute = 32, .second = 27 }, timestampToDateTime(1598607147));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -311,19 +79,3 @@ test "Convert timestamp to datetime" {
 | 
				
			||||||
    // Get time for date: https://wtools.io/convert-date-time-to-unix-time
 | 
					    // 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));
 | 
					    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,15 +14,6 @@ pub fn log(
 | 
				
			||||||
    // Ignore aws_signing messages
 | 
					    // Ignore aws_signing messages
 | 
				
			||||||
    if (verbose < 2 and scope == .aws_signing and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
 | 
					    if (verbose < 2 and scope == .aws_signing and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
 | 
				
			||||||
        return;
 | 
					        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
 | 
					    // Ignore awshttp messages
 | 
				
			||||||
    if (verbose < 1 and scope == .awshttp and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
 | 
					    if (verbose < 1 and scope == .awshttp and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
| 
						 | 
					@ -178,17 +169,18 @@ pub fn main() anyerror!void {
 | 
				
			||||||
                    std.log.err("no functions to work with", .{});
 | 
					                    std.log.err("no functions to work with", .{});
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            // TODO: This test fails with broken LLVM module
 | 
				
			||||||
            .ec2_query_no_input => {
 | 
					            .ec2_query_no_input => {
 | 
				
			||||||
 | 
					                std.log.err("EC2 Test disabled due to compiler bug", .{});
 | 
				
			||||||
                // Describe regions is a simpler request and easier to debug
 | 
					                // Describe regions is a simpler request and easier to debug
 | 
				
			||||||
                const result = try client.call(services.ec2.describe_regions.Request{}, options);
 | 
					                // const instances = try client.call(services.ec2.describe_regions.Request{}, options);
 | 
				
			||||||
                defer result.deinit();
 | 
					                // defer instances.deinit();
 | 
				
			||||||
                std.log.info("request id: {s}", .{result.response_metadata.request_id});
 | 
					                // std.log.info("region count: {d}", .{instances.response.regions.?.len});
 | 
				
			||||||
                std.log.info("region count: {d}", .{result.response.regions.?.len});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Describe instances is more interesting
 | 
					                // Describe instances is more interesting
 | 
				
			||||||
                const instances = try client.call(services.ec2.describe_instances.Request{}, options);
 | 
					                // const instances = try client.call(services.ec2.describe_instances.Request{}, options);
 | 
				
			||||||
                defer instances.deinit();
 | 
					                // defer instances.deinit();
 | 
				
			||||||
                std.log.info("reservation count: {d}", .{instances.response.reservations.?.len});
 | 
					                // std.log.info("reservation count: {d}", .{instances.response.reservations.len});
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        std.log.info("===== End Test: {s} =====\n", .{@tagName(t)});
 | 
					        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 {
 | 
					    pub fn getCharData(self: *Element, child_tag: []const u8) ?[]const u8 {
 | 
				
			||||||
        const child = (self.findChildByTag(child_tag) catch return null) orelse return null;
 | 
					        const child = self.findChildByTag(child_tag) orelse return null;
 | 
				
			||||||
        if (child.children.items.len != 1) {
 | 
					        if (child.children.items.len != 1) {
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,26 @@
 | 
				
			||||||
const std = @import("std");
 | 
					const std = @import("std");
 | 
				
			||||||
const xml = @import("xml.zig");
 | 
					const xml = @import("xml.zig");
 | 
				
			||||||
const date = @import("date.zig");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = std.log.scoped(.xml_shaper);
 | 
					const log = std.log.scoped(.xml_shaper);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn Parsed(comptime T: type) type {
 | 
					fn Parsed(comptime T: type) type {
 | 
				
			||||||
    return struct {
 | 
					    return struct {
 | 
				
			||||||
        // Forcing an arean allocator isn't my favorite choice here, but
 | 
					        allocator: std.mem.Allocator,
 | 
				
			||||||
        // is the simplest way to handle deallocation in the event of
 | 
					 | 
				
			||||||
        // an error
 | 
					 | 
				
			||||||
        allocator: std.heap.ArenaAllocator,
 | 
					 | 
				
			||||||
        parsed_value: T,
 | 
					        parsed_value: T,
 | 
				
			||||||
        document: xml.Document,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const Self = @This();
 | 
					        const Self = @This();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pub fn init(allocator: std.heap.ArenaAllocator, parsedObj: T, document: xml.Document) Self {
 | 
					        pub fn init(allocator: std.mem.Allocator, parsedObj: T) Self {
 | 
				
			||||||
            return .{
 | 
					            return .{
 | 
				
			||||||
                .allocator = allocator,
 | 
					                .allocator = allocator,
 | 
				
			||||||
                .parsed_value = parsedObj,
 | 
					                .parsed_value = parsedObj,
 | 
				
			||||||
                .document = document,
 | 
					 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pub fn deinit(self: Self) void {
 | 
					        pub fn deinit(self: Self) void {
 | 
				
			||||||
            self.allocator.deinit();
 | 
					            deinitObject(self.allocator, self.parsed_value);
 | 
				
			||||||
            // deinitObject(self.allocator, self.parsed_value);
 | 
					 | 
				
			||||||
            // self.document.deinit();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// This is dead code and can be removed with the move to ArenaAllocator
 | 
					 | 
				
			||||||
        fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
 | 
					        fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
 | 
				
			||||||
            switch (@typeInfo(@TypeOf(obj))) {
 | 
					            switch (@typeInfo(@TypeOf(obj))) {
 | 
				
			||||||
                .Optional => if (obj) |o| deinitObject(allocator, o),
 | 
					                .Optional => if (obj) |o| deinitObject(allocator, o),
 | 
				
			||||||
| 
						 | 
					@ -65,7 +54,28 @@ fn deinitObject(allocator: std.mem.Allocator, obj: anytype) void {
 | 
				
			||||||
                else => {},
 | 
					                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?
 | 
					// should we just use json parse options?
 | 
				
			||||||
pub const ParseOptions = struct {
 | 
					pub const ParseOptions = struct {
 | 
				
			||||||
    allocator: ?std.mem.Allocator = null,
 | 
					    allocator: ?std.mem.Allocator = null,
 | 
				
			||||||
| 
						 | 
					@ -76,17 +86,11 @@ pub fn parse(comptime T: type, source: []const u8, options: ParseOptions) !Parse
 | 
				
			||||||
    if (options.allocator == null)
 | 
					    if (options.allocator == null)
 | 
				
			||||||
        return error.AllocatorRequired; // we are only leaving it be null for compatibility with json
 | 
					        return error.AllocatorRequired; // we are only leaving it be null for compatibility with json
 | 
				
			||||||
    const allocator = options.allocator.?;
 | 
					    const allocator = options.allocator.?;
 | 
				
			||||||
    var arena_allocator = std.heap.ArenaAllocator.init(allocator);
 | 
					    var parse_allocator = std.heap.ArenaAllocator.init(allocator);
 | 
				
			||||||
    const aa = arena_allocator.allocator();
 | 
					    const parsed = try xml.parse(parse_allocator.allocator(), source);
 | 
				
			||||||
    errdefer arena_allocator.deinit();
 | 
					    // defer parsed.deinit(); // Let the arena allocator whack it all
 | 
				
			||||||
    const parsed = try xml.parse(aa, source);
 | 
					    defer parse_allocator.deinit();
 | 
				
			||||||
    errdefer parsed.deinit();
 | 
					    return Parsed(T).init(allocator, try parseInternal(T, parsed.root, options));
 | 
				
			||||||
    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 {
 | 
					fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions) !T {
 | 
				
			||||||
| 
						 | 
					@ -99,47 +103,10 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
 | 
				
			||||||
            return error.UnexpectedToken;
 | 
					            return error.UnexpectedToken;
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        .Float, .ComptimeFloat => {
 | 
					        .Float, .ComptimeFloat => {
 | 
				
			||||||
            return std.fmt.parseFloat(T, element.children.items[0].CharData) catch |e| {
 | 
					            return try std.fmt.parseFloat(T, element.children.items[0].CharData);
 | 
				
			||||||
                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 => {
 | 
					        .Int, .ComptimeInt => {
 | 
				
			||||||
            // 2021-10-05T16:39:45.000Z
 | 
					            return try std.fmt.parseInt(T, element.children.items[0].CharData, 10);
 | 
				
			||||||
            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| {
 | 
					        .Optional => |optional_info| {
 | 
				
			||||||
            if (element.children.items.len == 0) {
 | 
					            if (element.children.items.len == 0) {
 | 
				
			||||||
| 
						 | 
					@ -233,23 +200,13 @@ fn parseInternal(comptime T: type, element: *xml.Element, options: ParseOptions)
 | 
				
			||||||
                    //     }
 | 
					                    //     }
 | 
				
			||||||
                    // } else {
 | 
					                    // } else {
 | 
				
			||||||
                    log.debug("Found child element {s}", .{child.tag});
 | 
					                    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);
 | 
					                    @field(r, field.name) = try parseInternal(field.field_type, child, options);
 | 
				
			||||||
                    fields_seen[i] = true;
 | 
					                    fields_seen[i] = true;
 | 
				
			||||||
                    fields_set = fields_set + 1;
 | 
					                    fields_set = fields_set + 1;
 | 
				
			||||||
                    found_value = true;
 | 
					                    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
 | 
					                // Using this else clause breaks zig, so we'll use a boolean instead
 | 
				
			||||||
                if (!found_value) {
 | 
					                if (!found_value) return error.NoValueForField;
 | 
				
			||||||
                    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 {
 | 
					                // } else {
 | 
				
			||||||
                //     return error.NoValueForField;
 | 
					                //     return error.NoValueForField;
 | 
				
			||||||
                // }
 | 
					                // }
 | 
				
			||||||
| 
						 | 
					@ -459,21 +416,6 @@ test "can parse an optional boolean type" {
 | 
				
			||||||
    try testing.expectEqual(@as(?bool, true), parsed_data.parsed_value.foo_bar);
 | 
					    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
 | 
					// This is the simplest test so far that breaks zig
 | 
				
			||||||
test "can parse a boolean type (two fields)" {
 | 
					test "can parse a boolean type (two fields)" {
 | 
				
			||||||
    const allocator = std.testing.allocator;
 | 
					    const allocator = std.testing.allocator;
 | 
				
			||||||
| 
						 | 
					@ -492,28 +434,6 @@ test "can parse a boolean type (two fields)" {
 | 
				
			||||||
    defer parsed_data.deinit();
 | 
					    defer parsed_data.deinit();
 | 
				
			||||||
    try testing.expectEqual(@as(bool, true), parsed_data.parsed_value.foo_bar);
 | 
					    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" {
 | 
					test "can parse a nested type" {
 | 
				
			||||||
    const allocator = std.testing.allocator;
 | 
					    const allocator = std.testing.allocator;
 | 
				
			||||||
    const data =
 | 
					    const data =
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue