convert to json - core processing fully generic

This commit is contained in:
Emil Lerch 2021-05-13 15:53:53 -07:00
parent da5e58872f
commit 1b831cd91d
Signed by: lobo
GPG Key ID: A7B62D657EF764F8
2 changed files with 165 additions and 82 deletions

View File

@ -1,5 +1,5 @@
const std = @import("std"); const std = @import("std");
const xml = @import("xml.zig"); const json = @import("json.zig");
const c = @cImport({ const c = @cImport({
@cInclude("bitfield-workaround.h"); @cInclude("bitfield-workaround.h");
@cInclude("aws/common/allocator.h"); @cInclude("aws/common/allocator.h");
@ -67,17 +67,6 @@ fn ServiceActionResponse(comptime service: []const u8, comptime action: []const
arn: []const u8, arn: []const u8,
user_id: []const u8, user_id: []const u8,
account: []const u8, account: []const u8,
response_metadata: ResponseMetadata,
allocator: *std.mem.Allocator,
raw_response: xml.Document,
// If this is purely generic we won't be able to do it here as
// declarations aren't supported yet
// pub fn deinit(self: *const GetCallerIdentityResponse) void {
// self.responseMetadata.deinit();
// self.rawResponse.deinit();
// }
}; };
} }
unreachable; unreachable;
@ -165,13 +154,6 @@ pub const Aws = struct {
var tls_ctx_options: ?*c.aws_tls_ctx_options = null; var tls_ctx_options: ?*c.aws_tls_ctx_options = null;
var tls_ctx: ?*c.aws_tls_ctx = null; var tls_ctx: ?*c.aws_tls_ctx = null;
pub fn responseDeinit(raw_response: xml.Document, response_metadata: ?ResponseMetadata) void {
raw_response.deinit();
if (response_metadata) |meta| {
meta.deinit();
}
}
fn AsyncResult(comptime T: type) type { fn AsyncResult(comptime T: type) type {
return struct { return struct {
result: *T, result: *T,
@ -246,13 +228,14 @@ pub const Aws = struct {
log.debug("Deinit complete", .{}); log.debug("Deinit complete", .{});
} }
} }
pub fn call(self: Self, comptime request: anytype, options: Options) !Response(request) { pub fn call(self: Self, comptime request: anytype, options: Options) !FullResponse(request) {
const action_info = actionForRequest(request); const action_info = actionForRequest(request);
// This is true weirdness, but we are running into compiler bugs. Touch only if // This is true weirdness, but we are running into compiler bugs. Touch only if
// prepared... // prepared...
const service = @field(services, action_info.service); const service = @field(services, action_info.service);
const action = @field(service, action_info.action); const action = @field(service, action_info.action);
const R = Response(request); const R = Response(request);
const FullR = FullResponse(request);
log.debug("service {s}", .{action_info.service}); log.debug("service {s}", .{action_info.service});
log.debug("version {s}", .{service.version}); log.debug("version {s}", .{service.version});
@ -260,52 +243,37 @@ pub const Aws = struct {
const response = try self.callApi(action_info.service, service.version, action.action_name, options); const response = try self.callApi(action_info.service, service.version, action.action_name, options);
defer response.deinit(); defer response.deinit();
// TODO: Check status code for badness // TODO: Check status code for badness
const doc = try xml.parse(self.allocator, response.body); var stream = json.TokenStream.init(response.body);
const result = doc.root.findChildByTag("GetCallerIdentityResult");
return R{ const parser_options = json.ParseOptions{
.arn = result.?.getCharData("Arn").?,
.user_id = result.?.getCharData("UserId").?,
.account = result.?.getCharData("Account").?,
.allocator = self.allocator, .allocator = self.allocator,
.raw_response = doc, .allow_camel_case_conversion = true, // new option
.response_metadata = try metadataFromResponse(self.allocator, response.body), .allow_snake_case_conversion = true, // new option
.allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though
}; };
} const SResponse = ServerResponse(request);
fn actionForRequest(comptime request: anytype) struct { service: []const u8, action: []const u8, service_obj: anytype } { const parsed_response = try json.parse(SResponse, &stream, parser_options);
const type_name = @typeName(@TypeOf(request));
var service_start: usize = 0; // Grab the first (and only) object from the server. Server shape expected to be:
var service_end: usize = 0; // { ActionResponse: {ActionResult: {...}, ResponseMetadata: {...} } }
var action_start: usize = 0; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
var action_end: usize = 0; // Next line of code pulls this portion
for (type_name) |ch, i| { //
switch (ch) { //
'(' => service_start = i + 2, // And the response property below will pull whatever is the ActionResult object
')' => action_end = i - 1, // We can grab index [0] as structs are guaranteed by zig to be returned in the order
',' => { // declared, and we're declaring in that order in ServerResponse().
service_end = i - 1; const real_response = @field(parsed_response, @typeInfo(SResponse).Struct.fields[0].name);
action_start = i + 2; return FullR{
}, .response = @field(real_response, @typeInfo(@TypeOf(real_response)).Struct.fields[0].name),
else => continue, .response_metadata = .{
} .request_id = real_response.ResponseMetadata.RequestId,
} },
// const zero: usize = 0; .parser_options = parser_options,
// TODO: Figure out why if statement isn't working // .ParsedType = ServerResponse,
// if (serviceStart == zero or serviceEnd == zero or actionStart == zero or actionEnd == zero) { .raw_parsed = parsed_response,
// @compileLog("Type must be a function with two parameters \"service\" and \"action\". Found: " ++ type_name);
// // @compileError("Type must be a function with two parameters \"service\" and \"action\". Found: " ++ type_name);
// }
return .{
.service = type_name[service_start..service_end],
.action = type_name[action_start..action_end],
.service_obj = @field(services, type_name[service_start..service_end]),
}; };
} }
fn Response(comptime request: anytype) type {
const action_info = actionForRequest(request);
const service = @field(services, action_info.service);
const action = @field(service, action_info.action);
return action.Response;
}
fn callApi(self: Self, service: []const u8, version: []const u8, action: []const u8, options: Options) !HttpResult { fn callApi(self: Self, service: []const u8, version: []const u8, action: []const u8, options: Options) !HttpResult {
const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack); const endpoint = try regionSubDomain(self.allocator, service, options.region, options.dualstack);
defer endpoint.deinit(); defer endpoint.deinit();
@ -618,7 +586,7 @@ pub const Aws = struct {
fn addHeaders(self: Self, request: *c.aws_http_message, host: []const u8, body: []const u8) !void { fn addHeaders(self: Self, request: *c.aws_http_message, host: []const u8, body: []const u8) !void {
const accept_header = c.aws_http_header{ const accept_header = c.aws_http_header{
.name = c.aws_byte_cursor_from_c_str("Accept"), .name = c.aws_byte_cursor_from_c_str("Accept"),
.value = c.aws_byte_cursor_from_c_str("*/*"), .value = c.aws_byte_cursor_from_c_str("application/json"),
.compression = .AWS_HTTP_HEADER_COMPRESSION_USE_CACHE, .compression = .AWS_HTTP_HEADER_COMPRESSION_USE_CACHE,
}; };
if (c.aws_http_message_add_header(request, accept_header) != c.AWS_OP_SUCCESS) if (c.aws_http_message_add_header(request, accept_header) != c.AWS_OP_SUCCESS)
@ -962,16 +930,6 @@ fn cDeinit() void { // probably the wrong name
log.debug("auth clean up complete", .{}); log.debug("auth clean up complete", .{});
} }
pub const ResponseMetadata = struct {
request_id: ?[]const u8,
allocator: *std.mem.Allocator,
pub fn deinit(self: *const ResponseMetadata) void {
if (self.request_id) |id| {
self.allocator.free(id);
}
}
};
pub const Options = struct { pub const Options = struct {
region: []const u8 = "aws-global", region: []const u8 = "aws-global",
dualstack: bool = false, dualstack: bool = false,
@ -1143,3 +1101,103 @@ const RequestContext = struct {
}); });
} }
}; };
fn actionForRequest(comptime request: anytype) struct { service: []const u8, action: []const u8, service_obj: anytype } {
const type_name = @typeName(@TypeOf(request));
var service_start: usize = 0;
var service_end: usize = 0;
var action_start: usize = 0;
var action_end: usize = 0;
for (type_name) |ch, i| {
switch (ch) {
'(' => service_start = i + 2,
')' => action_end = i - 1,
',' => {
service_end = i - 1;
action_start = i + 2;
},
else => continue,
}
}
// const zero: usize = 0;
// TODO: Figure out why if statement isn't working
// if (serviceStart == zero or serviceEnd == zero or actionStart == zero or actionEnd == zero) {
// @compileLog("Type must be a function with two parameters \"service\" and \"action\". Found: " ++ type_name);
// // @compileError("Type must be a function with two parameters \"service\" and \"action\". Found: " ++ type_name);
// }
return .{
.service = type_name[service_start..service_end],
.action = type_name[action_start..action_end],
.service_obj = @field(services, type_name[service_start..service_end]),
};
}
fn ServerResponse(comptime request: anytype) type {
const T = Response(request);
const action_info = actionForRequest(request);
const service = @field(services, action_info.service);
const action = @field(service, action_info.action);
// NOTE: This is weird capitalization as a performance enhancement and to reduce
// allocations in json.zig
const ResponseMetadata = struct {
RequestId: []u8,
};
const Result = @Type(.{
.Struct = .{
.layout = .Auto,
.fields = &[_]std.builtin.TypeInfo.StructField{
.{
.name = action.action_name ++ "Result",
.field_type = T,
.default_value = null,
.is_comptime = false,
.alignment = 0,
},
.{
.name = "ResponseMetadata",
.field_type = ResponseMetadata,
.default_value = null,
.is_comptime = false,
.alignment = 0,
},
},
.decls = &[_]std.builtin.TypeInfo.Declaration{},
.is_tuple = false,
},
});
return @Type(.{
.Struct = .{
.layout = .Auto,
.fields = &[_]std.builtin.TypeInfo.StructField{
.{
.name = action.action_name ++ "Response",
.field_type = Result,
.default_value = null,
.is_comptime = false,
.alignment = 0,
},
},
.decls = &[_]std.builtin.TypeInfo.Declaration{},
.is_tuple = false,
},
});
}
fn FullResponse(comptime request: anytype) type {
return struct {
response: Response(request),
response_metadata: struct {
request_id: []u8,
},
parser_options: json.ParseOptions,
raw_parsed: ServerResponse(request),
const Self = @This();
pub fn deinit(self: Self) void {
json.parseFree(ServerResponse(request), self.raw_parsed, self.parser_options);
}
};
}
fn Response(comptime request: anytype) type {
const action_info = actionForRequest(request);
const service = @field(services, action_info.service);
const action = @field(service, action_info.action);
return action.Response;
}

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const aws = @import("aws.zig"); const aws = @import("aws.zig");
const json = @import("json.zig");
pub fn log( pub fn log(
comptime level: std.log.Level, comptime level: std.log.Level,
@ -10,7 +11,6 @@ pub fn log(
// Ignore awshttp messages // Ignore awshttp messages
if (scope == .awshttp and @enumToInt(level) >= @enumToInt(std.log.Level.debug)) if (scope == .awshttp and @enumToInt(level) >= @enumToInt(std.log.Level.debug))
return; return;
const scope_prefix = "(" ++ @tagName(scope) ++ "): "; const scope_prefix = "(" ++ @tagName(scope) ++ "): ";
const prefix = "[" ++ @tagName(level) ++ "] " ++ scope_prefix; const prefix = "[" ++ @tagName(level) ++ "] " ++ scope_prefix;
@ -27,6 +27,15 @@ pub fn main() anyerror!void {
// defer file.close(); // defer file.close();
// var child_allocator = std.heap.c_allocator; // var child_allocator = std.heap.c_allocator;
// const allocator = &std.heap.loggingAllocator(child_allocator, file.writer()).allocator; // const allocator = &std.heap.loggingAllocator(child_allocator, file.writer()).allocator;
// Flip to true to run a second time. This will help debug
// allocation/deallocation issues
const test_twice = false;
// Flip to true to run through the json parsing changes made to stdlib
const test_json = false;
if (test_json) try jsonFun();
const allocator = std.heap.c_allocator; const allocator = std.heap.c_allocator;
const options = aws.Options{ const options = aws.Options{
@ -39,11 +48,8 @@ pub fn main() anyerror!void {
const resp = try client.call(aws.services.sts.get_caller_identity.Request{}, options); const resp = try client.call(aws.services.sts.get_caller_identity.Request{}, options);
// TODO: This is a bit wonky. Root cause is lack of declarations in // TODO: This is a bit wonky. Root cause is lack of declarations in
// comptime-generated types // comptime-generated types
defer aws.Aws.responseDeinit(resp.raw_response, resp.response_metadata); defer resp.deinit();
// Flip to true to run a second time. This will help debug
// allocation/deallocation issues
const test_twice = false;
if (test_twice) { if (test_twice) {
std.time.sleep(1000 * std.time.ns_per_ms); std.time.sleep(1000 * std.time.ns_per_ms);
std.log.info("second request", .{}); std.log.info("second request", .{});
@ -51,13 +57,32 @@ pub fn main() anyerror!void {
var client2 = aws.Aws.init(allocator); var client2 = aws.Aws.init(allocator);
defer client2.deinit(); defer client2.deinit();
const resp2 = try client2.call(aws.services.sts.get_caller_identity.Request{}, options); // catch here and try alloc? const resp2 = try client2.call(aws.services.sts.get_caller_identity.Request{}, options); // catch here and try alloc?
defer aws.Aws.responseDeinit(resp2.raw_response, resp2.response_metadata); defer resp2.deinit();
} }
std.log.info("arn: {s}", .{resp.arn}); std.log.info("arn: {s}", .{resp.response.arn});
std.log.info("id: {s}", .{resp.user_id}); std.log.info("id: {s}", .{resp.response.user_id});
std.log.info("account: {s}", .{resp.account}); std.log.info("account: {s}", .{resp.response.account});
std.log.info("requestId: {s}", .{resp.response_metadata.request_id}); std.log.info("requestId: {s}", .{resp.response_metadata.request_id});
std.log.info("Departing main", .{}); std.log.info("Departing main", .{});
} }
pub fn jsonFun() !void {
// Standard behavior
const payload =
\\{"GetCallerIdentityResponse":{"GetCallerIdentityResult":{"Account":"0123456789","Arn":"arn:aws:iam::0123456789:user/test","UserId":"MYUSERID"},"ResponseMetadata":{"RequestId":"3b80a99b-7df8-4bcb-96ee-b2759878a5f2"}}}
;
const Ret3 = struct {
getCallerIdentityResponse: struct { getCallerIdentityResult: struct { account: []u8, arn: []u8, user_id: []u8 }, responseMetadata: struct { requestId: []u8 } },
};
var stream3 = json.TokenStream.init(payload);
const res3 = json.parse(Ret3, &stream3, .{
.allocator = std.heap.c_allocator,
.allow_camel_case_conversion = true, // new option
.allow_snake_case_conversion = true, // new option
.allow_unknown_fields = true, // new option
}) catch unreachable;
std.log.info("{}", .{res3});
std.log.info("{s}", .{res3.getCallerIdentityResponse.getCallerIdentityResult.user_id});
}