986 lines
32 KiB
Zig
986 lines
32 KiB
Zig
const std = @import("std");
|
|
const expect = std.testing.expect;
|
|
|
|
// TODO: validate this matches the schema
|
|
pub const Smithy = struct {
|
|
version: []const u8,
|
|
metadata: ModelMetadata,
|
|
shapes: []ShapeInfo,
|
|
allocator: std.mem.Allocator,
|
|
json_source: std.json.Parsed(std.json.Value),
|
|
|
|
const Self = @This();
|
|
pub fn init(allocator: std.mem.Allocator, version: []const u8, metadata: ModelMetadata, shapeinfo: []ShapeInfo, json_source: std.json.Parsed(std.json.Value)) Smithy {
|
|
return .{
|
|
.version = version,
|
|
.metadata = metadata,
|
|
.shapes = shapeinfo,
|
|
.allocator = allocator,
|
|
.json_source = json_source,
|
|
};
|
|
}
|
|
pub fn deinit(self: Self) void {
|
|
for (self.shapes) |s| {
|
|
switch (s.shape) {
|
|
.string,
|
|
.byte,
|
|
.short,
|
|
.integer,
|
|
.long,
|
|
.float,
|
|
.double,
|
|
.bigInteger,
|
|
.bigDecimal,
|
|
.blob,
|
|
.boolean,
|
|
.timestamp,
|
|
.document,
|
|
.member,
|
|
.resource,
|
|
=> |v| self.allocator.free(v.traits),
|
|
.structure => |v| {
|
|
for (v.members) |m| self.allocator.free(m.traits);
|
|
self.allocator.free(v.members);
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.uniontype => |v| {
|
|
for (v.members) |m| self.allocator.free(m.traits);
|
|
self.allocator.free(v.members);
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.service => |v| {
|
|
self.allocator.free(v.traits);
|
|
self.allocator.free(v.operations);
|
|
},
|
|
.operation => |v| {
|
|
if (v.errors) |e| self.allocator.free(e);
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.list => |v| {
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.set => |v| {
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.map => |v| {
|
|
self.allocator.free(v.key);
|
|
self.allocator.free(v.value);
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.@"enum" => |v| {
|
|
for (v.members) |m| self.allocator.free(m.traits);
|
|
self.allocator.free(v.members);
|
|
self.allocator.free(v.traits);
|
|
},
|
|
.unit => |v| {
|
|
self.allocator.free(v.traits);
|
|
},
|
|
}
|
|
}
|
|
self.allocator.free(self.shapes);
|
|
self.json_source.deinit();
|
|
}
|
|
};
|
|
pub const ShapeInfo = struct {
|
|
id: []const u8,
|
|
namespace: []const u8,
|
|
name: []const u8,
|
|
member: ?[]const u8,
|
|
|
|
shape: Shape,
|
|
};
|
|
|
|
const ModelMetadata = struct {
|
|
suppressions: []struct {
|
|
id: []const u8,
|
|
namespace: []const u8,
|
|
},
|
|
};
|
|
|
|
pub const TraitType = enum {
|
|
aws_api_service,
|
|
aws_auth_sigv4,
|
|
aws_protocol,
|
|
ec2_query_name,
|
|
http,
|
|
http_header,
|
|
http_label,
|
|
http_query,
|
|
http_payload,
|
|
json_name,
|
|
xml_name,
|
|
required, // required on the server
|
|
client_optional, // optional as far as the client is concerned
|
|
documentation,
|
|
pattern,
|
|
range,
|
|
length,
|
|
box,
|
|
sparse,
|
|
enum_value,
|
|
aws_query_error,
|
|
};
|
|
pub const Trait = union(TraitType) {
|
|
aws_api_service: struct {
|
|
sdk_id: []const u8,
|
|
arn_namespace: ?[]const u8,
|
|
cloudformation_name: ?[]const u8,
|
|
cloudtrail_event_source: ?[]const u8,
|
|
endpoint_prefix: []const u8,
|
|
},
|
|
aws_auth_sigv4: struct {
|
|
name: []const u8,
|
|
},
|
|
aws_protocol: AwsProtocol,
|
|
ec2_query_name: []const u8,
|
|
http: struct {
|
|
method: []const u8,
|
|
uri: []const u8,
|
|
code: i64 = 200,
|
|
},
|
|
http_header: []const u8,
|
|
http_label: []const u8,
|
|
http_query: []const u8,
|
|
http_payload: struct {},
|
|
json_name: []const u8,
|
|
xml_name: []const u8,
|
|
required: struct {},
|
|
client_optional: void,
|
|
documentation: []const u8,
|
|
pattern: []const u8,
|
|
range: struct { // most data is actually integers, but as some are floats, we'll use that here
|
|
min: ?f64,
|
|
max: ?f64,
|
|
},
|
|
length: struct {
|
|
min: ?f64,
|
|
max: ?f64,
|
|
},
|
|
box: struct {},
|
|
sparse: struct {},
|
|
enum_value: []const u8,
|
|
aws_query_error: struct {
|
|
http_response_code: i64,
|
|
code: []const u8,
|
|
},
|
|
};
|
|
const ShapeType = enum {
|
|
blob,
|
|
boolean,
|
|
string,
|
|
byte,
|
|
short,
|
|
integer,
|
|
long,
|
|
float,
|
|
double,
|
|
bigInteger,
|
|
bigDecimal,
|
|
timestamp,
|
|
document,
|
|
member,
|
|
list,
|
|
set,
|
|
map,
|
|
structure,
|
|
uniontype,
|
|
service,
|
|
operation,
|
|
resource,
|
|
@"enum",
|
|
unit,
|
|
};
|
|
const TraitsOnly = struct {
|
|
traits: []Trait,
|
|
};
|
|
pub const TypeMember = struct {
|
|
name: []const u8,
|
|
target: []const u8,
|
|
traits: []Trait,
|
|
};
|
|
const Shape = union(ShapeType) {
|
|
blob: TraitsOnly,
|
|
boolean: TraitsOnly,
|
|
string: TraitsOnly,
|
|
byte: TraitsOnly,
|
|
short: TraitsOnly,
|
|
integer: TraitsOnly,
|
|
long: TraitsOnly,
|
|
float: TraitsOnly,
|
|
double: TraitsOnly,
|
|
bigInteger: TraitsOnly,
|
|
bigDecimal: TraitsOnly,
|
|
timestamp: TraitsOnly,
|
|
document: TraitsOnly,
|
|
member: TraitsOnly,
|
|
list: struct {
|
|
member_target: []const u8,
|
|
traits: []Trait,
|
|
},
|
|
set: struct {
|
|
member_target: []const u8,
|
|
traits: []Trait,
|
|
},
|
|
map: struct {
|
|
key: []const u8,
|
|
value: []const u8,
|
|
traits: []Trait,
|
|
},
|
|
structure: struct {
|
|
members: []TypeMember,
|
|
traits: []Trait,
|
|
},
|
|
uniontype: struct {
|
|
members: []TypeMember,
|
|
traits: []Trait,
|
|
},
|
|
service: struct {
|
|
version: []const u8,
|
|
operations: [][]const u8,
|
|
resources: [][]const u8,
|
|
traits: []Trait,
|
|
},
|
|
operation: struct {
|
|
input: ?[]const u8,
|
|
output: ?[]const u8,
|
|
errors: ?[][]const u8,
|
|
traits: []Trait,
|
|
},
|
|
resource: TraitsOnly,
|
|
@"enum": struct {
|
|
members: []TypeMember,
|
|
traits: []Trait,
|
|
},
|
|
unit: TraitsOnly,
|
|
};
|
|
|
|
// https://awslabs.github.io/smithy/1.0/spec/aws/index.html
|
|
pub const AwsProtocol = enum {
|
|
query,
|
|
rest_xml,
|
|
json_1_1,
|
|
json_1_0,
|
|
rest_json_1,
|
|
ec2_query,
|
|
};
|
|
|
|
pub fn parse(allocator: std.mem.Allocator, json_model: []const u8) !Smithy {
|
|
// construct a parser. We're not copying strings here
|
|
// Instead, we keep the original json string around
|
|
// This might be bad if we only need a small fraction of the original json source
|
|
var vt = try std.json.parseFromSlice(std.json.Value, allocator, json_model, .{});
|
|
return Smithy.init(
|
|
allocator,
|
|
vt.value.object.get("smithy").?.string,
|
|
ModelMetadata{
|
|
// TODO: implement
|
|
.suppressions = &.{},
|
|
},
|
|
try shapes(allocator, vt.value.object.get("shapes").?.object),
|
|
vt,
|
|
);
|
|
}
|
|
|
|
// anytype: HashMap([]const u8, std.json.Value...)
|
|
// list must be deinitialized by caller
|
|
fn shapes(allocator: std.mem.Allocator, map: anytype) ![]ShapeInfo {
|
|
var list = try std.ArrayList(ShapeInfo).initCapacity(allocator, map.count());
|
|
defer list.deinit();
|
|
var iterator = map.iterator();
|
|
while (iterator.next()) |kv| {
|
|
const id_info = try parseId(kv.key_ptr.*);
|
|
try list.append(.{
|
|
.id = id_info.id,
|
|
.namespace = id_info.namespace,
|
|
.name = id_info.name,
|
|
.member = id_info.member,
|
|
.shape = getShape(allocator, kv.value_ptr.*) catch |e| {
|
|
std.log.err("Caught error parsing shape with name {s}: {}", .{ id_info.name, e });
|
|
return e;
|
|
},
|
|
});
|
|
}
|
|
// This seems to be a synonym for the simple type "string"
|
|
// https://awslabs.github.io/smithy/1.0/spec/core/model.html#simple-types
|
|
// But I don't see it in the spec. We might need to preload other similar
|
|
// simple types?
|
|
try list.append(.{
|
|
.id = "smithy.api#String",
|
|
.namespace = "smithy.api",
|
|
.name = "String",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.string = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Boolean",
|
|
.namespace = "smithy.api",
|
|
.name = "Boolean",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.boolean = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Integer",
|
|
.namespace = "smithy.api",
|
|
.name = "Integer",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.integer = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Double",
|
|
.namespace = "smithy.api",
|
|
.name = "Double",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.double = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Timestamp",
|
|
.namespace = "smithy.api",
|
|
.name = "Timestamp",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.timestamp = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Blob",
|
|
.namespace = "smithy.api",
|
|
.name = "Blob",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.blob = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Unit",
|
|
.namespace = "smithy.api",
|
|
.name = "Unit",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.unit = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Long",
|
|
.namespace = "smithy.api",
|
|
.name = "Long",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.long = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Float",
|
|
.namespace = "smithy.api",
|
|
.name = "Float",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.float = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#Document",
|
|
.namespace = "smithy.api",
|
|
.name = "Document",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.document = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
// These "Primitive" versions only appear to differ in that they have defaults
|
|
// defined. Not currently handled:
|
|
// byte PrimitiveByte
|
|
// short PrimitiveShort
|
|
|
|
try list.append(.{
|
|
.id = "smithy.api#PrimitiveBoolean",
|
|
.namespace = "smithy.api",
|
|
.name = "PrimitiveBoolean",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.boolean = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#PrimitiveInteger",
|
|
.namespace = "smithy.api",
|
|
.name = "PrimitiveInteger",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.integer = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#PrimitiveDouble",
|
|
.namespace = "smithy.api",
|
|
.name = "PrimitiveDouble",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.double = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#PrimitiveLong",
|
|
.namespace = "smithy.api",
|
|
.name = "PrimitiveLong",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.long = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
try list.append(.{
|
|
.id = "smithy.api#PrimitiveFloat",
|
|
.namespace = "smithy.api",
|
|
.name = "PrimitiveFloat",
|
|
.member = null,
|
|
.shape = Shape{
|
|
.float = .{
|
|
.traits = &.{},
|
|
},
|
|
},
|
|
});
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
fn getShape(allocator: std.mem.Allocator, shape: std.json.Value) SmithyParseError!Shape {
|
|
const shape_type = shape.object.get("type").?.string;
|
|
if (std.mem.eql(u8, shape_type, "service"))
|
|
return Shape{
|
|
.service = .{
|
|
.version = shape.object.get("version").?.string,
|
|
.operations = if (shape.object.get("operations")) |ops|
|
|
try parseTargetList(allocator, ops.array)
|
|
else
|
|
&.{}, // this doesn't make much sense, but it's happening
|
|
// TODO: implement. We need some sample data tho
|
|
.resources = &.{},
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "structure"))
|
|
return Shape{
|
|
.structure = .{
|
|
.members = try parseMembers(allocator, shape.object.get("members")),
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "union"))
|
|
return Shape{
|
|
.uniontype = .{
|
|
.members = try parseMembers(allocator, shape.object.get("members")),
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "operation"))
|
|
return Shape{
|
|
.operation = .{
|
|
.input = if (shape.object.get("input")) |member| member.object.get("target").?.string else null,
|
|
.output = if (shape.object.get("output")) |member| member.object.get("target").?.string else null,
|
|
.errors = blk: {
|
|
if (shape.object.get("errors")) |e| {
|
|
break :blk try parseTargetList(allocator, e.array);
|
|
}
|
|
break :blk null;
|
|
},
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "list"))
|
|
return Shape{
|
|
.list = .{
|
|
.member_target = shape.object.get("member").?.object.get("target").?.string,
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "set"))
|
|
return Shape{
|
|
.set = .{
|
|
.member_target = shape.object.get("member").?.object.get("target").?.string,
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "map"))
|
|
return Shape{
|
|
.map = .{
|
|
.key = shape.object.get("key").?.object.get("target").?.string,
|
|
.value = shape.object.get("value").?.object.get("target").?.string,
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, shape_type, "string"))
|
|
return Shape{ .string = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "byte"))
|
|
return Shape{ .byte = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "short"))
|
|
return Shape{ .short = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "integer"))
|
|
return Shape{ .integer = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "long"))
|
|
return Shape{ .long = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "float"))
|
|
return Shape{ .float = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "double"))
|
|
return Shape{ .double = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "bigInteger"))
|
|
return Shape{ .bigInteger = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "bigDecimal"))
|
|
return Shape{ .bigDecimal = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "boolean"))
|
|
return Shape{ .boolean = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "blob"))
|
|
return Shape{ .blob = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "timestamp"))
|
|
return Shape{ .timestamp = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "document"))
|
|
return Shape{ .document = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "member"))
|
|
return Shape{ .member = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "resource"))
|
|
return Shape{ .resource = try parseTraitsOnly(allocator, shape) };
|
|
if (std.mem.eql(u8, shape_type, "enum"))
|
|
return Shape{
|
|
.@"enum" = .{
|
|
.members = try parseMembers(allocator, shape.object.get("members")),
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
},
|
|
};
|
|
|
|
std.debug.print("Invalid Type: {s}", .{shape_type});
|
|
return SmithyParseError.InvalidType;
|
|
}
|
|
|
|
fn parseMembers(allocator: std.mem.Allocator, shape: ?std.json.Value) SmithyParseError![]TypeMember {
|
|
const rc: []TypeMember = &.{};
|
|
if (shape == null)
|
|
return rc;
|
|
|
|
const map = shape.?.object;
|
|
var list = std.ArrayList(TypeMember).initCapacity(allocator, map.count()) catch return SmithyParseError.OutOfMemory;
|
|
defer list.deinit();
|
|
var iterator = map.iterator();
|
|
while (iterator.next()) |kv| {
|
|
try list.append(TypeMember{
|
|
.name = kv.key_ptr.*,
|
|
.target = kv.value_ptr.*.object.get("target").?.string,
|
|
.traits = try parseTraits(allocator, kv.value_ptr.*.object.get("traits")),
|
|
});
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
// ArrayList of std.Json.Value
|
|
fn parseTargetList(allocator: std.mem.Allocator, list: anytype) SmithyParseError![][]const u8 {
|
|
var array_list = std.ArrayList([]const u8).initCapacity(allocator, list.items.len) catch return SmithyParseError.OutOfMemory;
|
|
defer array_list.deinit();
|
|
for (list.items) |i| {
|
|
try array_list.append(i.object.get("target").?.string);
|
|
}
|
|
return array_list.toOwnedSlice();
|
|
}
|
|
fn parseTraitsOnly(allocator: std.mem.Allocator, shape: std.json.Value) SmithyParseError!TraitsOnly {
|
|
return TraitsOnly{
|
|
.traits = try parseTraits(allocator, shape.object.get("traits")),
|
|
};
|
|
}
|
|
|
|
fn parseTraits(allocator: std.mem.Allocator, shape: ?std.json.Value) SmithyParseError![]Trait {
|
|
const rc: []Trait = &.{};
|
|
if (shape == null)
|
|
return rc;
|
|
|
|
const map = shape.?.object;
|
|
var list = std.ArrayList(Trait).initCapacity(allocator, map.count()) catch return SmithyParseError.OutOfMemory;
|
|
defer list.deinit();
|
|
var iterator = map.iterator();
|
|
while (iterator.next()) |kv| {
|
|
if (try getTrait(kv.key_ptr.*, kv.value_ptr.*)) |t|
|
|
try list.append(t);
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
fn getTrait(trait_type: []const u8, value: std.json.Value) SmithyParseError!?Trait {
|
|
if (std.mem.eql(u8, trait_type, "aws.api#service"))
|
|
return Trait{
|
|
.aws_api_service = .{
|
|
.sdk_id = value.object.get("sdkId").?.string,
|
|
.arn_namespace = if (value.object.get("arnNamespace")) |a| a.string else null,
|
|
.cloudformation_name = if (value.object.get("cloudFormationName")) |n| n.string else null,
|
|
.cloudtrail_event_source = if (value.object.get("cloudTrailEventSource")) |s| s.string else null,
|
|
// what good is a service without an endpoint? I don't know - ask amp
|
|
.endpoint_prefix = if (value.object.get("endpointPrefix")) |endpoint| endpoint.string else "",
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.auth#sigv4"))
|
|
return Trait{
|
|
.aws_auth_sigv4 = .{
|
|
.name = value.object.get("name").?.string,
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#required"))
|
|
return Trait{ .required = .{} };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#clientOptional"))
|
|
return Trait{ .client_optional = {} };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#sparse"))
|
|
return Trait{ .sparse = .{} };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#box"))
|
|
return Trait{ .box = .{} };
|
|
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#range"))
|
|
return Trait{
|
|
.range = .{
|
|
.min = getOptionalNumber(value, "min"),
|
|
.max = getOptionalNumber(value, "max"),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#length"))
|
|
return Trait{
|
|
.length = .{
|
|
.min = getOptionalNumber(value, "min"),
|
|
.max = getOptionalNumber(value, "max"),
|
|
},
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#restJson1"))
|
|
return Trait{
|
|
.aws_protocol = .rest_json_1,
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#awsJson1_0"))
|
|
return Trait{
|
|
.aws_protocol = .json_1_0,
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#awsJson1_1"))
|
|
return Trait{
|
|
.aws_protocol = .json_1_1,
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#restXml"))
|
|
return Trait{
|
|
.aws_protocol = .rest_xml,
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#awsQuery"))
|
|
return Trait{
|
|
.aws_protocol = .query,
|
|
};
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#ec2Query"))
|
|
return Trait{
|
|
.aws_protocol = .ec2_query,
|
|
};
|
|
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#documentation"))
|
|
return Trait{ .documentation = value.string };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#pattern"))
|
|
return Trait{ .pattern = value.string };
|
|
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#ec2QueryName"))
|
|
return Trait{ .ec2_query_name = value.string };
|
|
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#http")) {
|
|
var code: i64 = 200;
|
|
if (value.object.get("code")) |v| {
|
|
if (v == .integer)
|
|
code = v.integer;
|
|
}
|
|
return Trait{ .http = .{
|
|
.method = value.object.get("method").?.string,
|
|
.uri = value.object.get("uri").?.string,
|
|
.code = code,
|
|
} };
|
|
}
|
|
if (std.mem.eql(u8, trait_type, "aws.protocols#awsQueryError")) {
|
|
return Trait{
|
|
.aws_query_error = .{
|
|
.code = value.object.get("code").?.string, // code is required
|
|
.http_response_code = value.object.get("httpResponseCode").?.integer,
|
|
},
|
|
};
|
|
}
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#jsonName"))
|
|
return Trait{ .json_name = value.string };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#xmlName"))
|
|
return Trait{ .xml_name = value.string };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#httpQuery"))
|
|
return Trait{ .http_query = value.string };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#httpHeader"))
|
|
return Trait{ .http_header = value.string };
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#httpPayload"))
|
|
return Trait{ .http_payload = .{} };
|
|
|
|
// TODO: Maybe care about these traits?
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#title"))
|
|
return null;
|
|
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#xmlNamespace"))
|
|
return null;
|
|
|
|
if (std.mem.eql(u8, trait_type, "smithy.api#enumValue"))
|
|
return Trait{ .enum_value = value.string };
|
|
|
|
// TODO: win argument with compiler to get this comptime
|
|
const list =
|
|
\\aws.api#arnReference
|
|
\\aws.api#clientDiscoveredEndpoint
|
|
\\aws.api#clientEndpointDiscovery
|
|
\\aws.api#arn
|
|
\\aws.auth#unsignedPayload
|
|
\\aws.iam#disableConditionKeyInference
|
|
\\smithy.api#auth
|
|
\\smithy.api#cors
|
|
\\smithy.api#deprecated
|
|
\\smithy.api#endpoint
|
|
\\smithy.api#enum
|
|
\\smithy.api#error
|
|
\\smithy.api#eventPayload
|
|
\\smithy.api#externalDocumentation
|
|
\\smithy.api#hostLabel
|
|
\\smithy.api#httpError
|
|
\\smithy.api#httpChecksumRequired
|
|
\\smithy.api#httpLabel
|
|
\\smithy.api#httpPrefixHeaders
|
|
\\smithy.api#httpQueryParams
|
|
\\smithy.api#httpResponseCode
|
|
\\smithy.api#idempotencyToken
|
|
\\smithy.api#idempotent
|
|
\\smithy.api#mediaType
|
|
\\smithy.api#noReplace
|
|
\\smithy.api#optionalAuth
|
|
\\smithy.api#paginated
|
|
\\smithy.api#readonly
|
|
\\smithy.api#references
|
|
\\smithy.api#requiresLength
|
|
\\smithy.api#retryable
|
|
\\smithy.api#sensitive
|
|
\\smithy.api#streaming
|
|
\\smithy.api#suppress
|
|
\\smithy.api#tags
|
|
\\smithy.api#timestampFormat
|
|
\\smithy.api#xmlAttribute
|
|
\\smithy.api#xmlFlattened
|
|
\\smithy.waiters#waitable
|
|
\\smithy.rules#endpointTests
|
|
\\smithy.api#input
|
|
\\smithy.api#output
|
|
\\smithy.api#default
|
|
\\smithy.api#examples
|
|
\\smithy.api#uniqueItems
|
|
\\smithy.api#addedDefault
|
|
\\smithy.api#resourceIdentifier
|
|
\\smithy.api#unstable
|
|
\\smithy.api#property
|
|
\\smithy.api#notProperty
|
|
\\smithy.api#recommended
|
|
\\smithy.api#httpBearerAuth
|
|
\\smithy.api#nestedProperties
|
|
\\smithy.rules#endpointRuleSet
|
|
\\smithy.rules#contextParam
|
|
\\smithy.rules#clientContextParams
|
|
\\smithy.rules#staticContextParams
|
|
\\aws.cloudformation#cfnResource
|
|
\\aws.cloudformation#cfnMutability
|
|
\\aws.cloudformation#cfnExcludeProperty
|
|
\\aws.cloudformation#cfnAdditionalIdentifier
|
|
\\aws.iam#actionPermissionDescription
|
|
\\aws.iam#requiredActions
|
|
\\aws.iam#conditionKeys
|
|
\\aws.iam#iamResource
|
|
\\aws.iam#iamAction
|
|
\\aws.iam#supportedPrincipalTypes
|
|
\\aws.iam#defineConditionKeys
|
|
\\aws.iam#actionName
|
|
\\aws.api#data
|
|
\\aws.api#controlPlane
|
|
\\aws.api#dataPlane
|
|
\\aws.api#tagEnabled
|
|
\\aws.api#taggable
|
|
\\aws.protocols#awsQueryCompatible
|
|
\\aws.protocols#httpChecksum
|
|
\\aws.customizations#s3UnwrappedXmlOutput
|
|
; // NOTE: inputs/outputs are not used in AWS models, but default is and might be handy
|
|
var iterator = std.mem.split(u8, list, "\n");
|
|
while (iterator.next()) |known_but_unimplemented| {
|
|
if (std.mem.eql(u8, trait_type, known_but_unimplemented))
|
|
return null;
|
|
}
|
|
|
|
// Totally unknown type
|
|
std.log.err("Invalid Trait Type: {s}", .{trait_type});
|
|
return null;
|
|
}
|
|
fn getOptionalNumber(value: std.json.Value, key: []const u8) ?f64 {
|
|
if (value.object.get(key)) |v|
|
|
return switch (v) {
|
|
.integer => @as(f64, @floatFromInt(v.integer)),
|
|
.float => v.float,
|
|
.null, .bool, .number_string, .string, .array, .object => null,
|
|
};
|
|
return null;
|
|
}
|
|
const IdInfo = struct { id: []const u8, namespace: []const u8, name: []const u8, member: ?[]const u8 };
|
|
const SmithyParseError = error{
|
|
NoHashtagFound,
|
|
InvalidType,
|
|
OutOfMemory,
|
|
};
|
|
fn parseId(id: []const u8) SmithyParseError!IdInfo {
|
|
var hashtag: ?usize = null;
|
|
var dollar: ?usize = null;
|
|
var inx: usize = 0;
|
|
for (id) |ch| {
|
|
switch (ch) {
|
|
'#' => hashtag = inx,
|
|
'$' => dollar = inx,
|
|
else => {},
|
|
}
|
|
inx = inx + 1;
|
|
}
|
|
if (hashtag == null) {
|
|
std.debug.print("no hashtag found on id: {s}\n", .{id});
|
|
return SmithyParseError.NoHashtagFound;
|
|
}
|
|
const namespace = id[0..hashtag.?];
|
|
var end = id.len;
|
|
var member: ?[]const u8 = null;
|
|
if (dollar) |d| {
|
|
member = id[dollar.? + 1 .. end];
|
|
end = d;
|
|
}
|
|
const name = id[hashtag.? + 1 .. end];
|
|
return IdInfo{
|
|
.id = id,
|
|
.namespace = namespace,
|
|
.name = name,
|
|
.member = member,
|
|
};
|
|
}
|
|
fn read_file_to_string(allocator: std.mem.Allocator, file_name: []const u8, max_bytes: usize) ![]const u8 {
|
|
const file = try std.fs.cwd().openFile(file_name, std.fs.File.OpenFlags{});
|
|
defer file.close();
|
|
return file.readToEndAlloc(allocator, max_bytes);
|
|
}
|
|
const test_data: []const u8 = @embedFile("test.json");
|
|
const intrinsic_type_count: usize = 15; // 15 intrinsic types are added to every model (see shapes() function)
|
|
|
|
test "parse string" {
|
|
const test_string =
|
|
\\ {
|
|
\\ "smithy": "1.0",
|
|
\\ "shapes": {
|
|
\\ "com.amazonaws.sts#AWSSecurityTokenServiceV20110615": {
|
|
\\ "type": "service",
|
|
\\ "version": "2011-06-15",
|
|
\\ "operations": [
|
|
\\ {
|
|
\\ "target": "op"
|
|
\\ }
|
|
\\ ]
|
|
\\ }
|
|
\\ }
|
|
\\ }
|
|
\\
|
|
\\
|
|
;
|
|
|
|
const allocator = std.testing.allocator;
|
|
const model = try parse(allocator, test_string);
|
|
defer model.deinit();
|
|
try expect(std.mem.eql(u8, model.version, "1.0"));
|
|
|
|
try std.testing.expectEqual(intrinsic_type_count + 1, model.shapes.len);
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts#AWSSecurityTokenServiceV20110615", model.shapes[0].id);
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts", model.shapes[0].namespace);
|
|
try std.testing.expectEqualStrings("AWSSecurityTokenServiceV20110615", model.shapes[0].name);
|
|
try std.testing.expect(model.shapes[0].member == null);
|
|
try std.testing.expectEqualStrings("2011-06-15", model.shapes[0].shape.service.version);
|
|
}
|
|
test "parse shape with member" {
|
|
const test_string =
|
|
\\ {
|
|
\\ "smithy": "1.0",
|
|
\\ "shapes": {
|
|
\\ "com.amazonaws.sts#AWSSecurityTokenServiceV20110615$member": {
|
|
\\ "type": "service",
|
|
\\ "version": "2011-06-15",
|
|
\\ "operations": [
|
|
\\ {
|
|
\\ "target": "op"
|
|
\\ }
|
|
\\ ]
|
|
\\ }
|
|
\\ }
|
|
\\ }
|
|
\\
|
|
\\
|
|
;
|
|
|
|
const allocator = std.testing.allocator;
|
|
const model = try parse(allocator, test_string);
|
|
defer model.deinit();
|
|
try expect(std.mem.eql(u8, model.version, "1.0"));
|
|
try std.testing.expectEqual(intrinsic_type_count + 1, model.shapes.len);
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts#AWSSecurityTokenServiceV20110615$member", model.shapes[0].id);
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts", model.shapes[0].namespace);
|
|
try std.testing.expectEqualStrings("AWSSecurityTokenServiceV20110615", model.shapes[0].name);
|
|
try std.testing.expectEqualStrings("2011-06-15", model.shapes[0].shape.service.version);
|
|
try std.testing.expectEqualStrings("member", model.shapes[0].member.?);
|
|
}
|
|
test "parse file" {
|
|
const allocator = std.testing.allocator;
|
|
const model = try parse(allocator, test_data);
|
|
defer model.deinit();
|
|
try std.testing.expectEqualStrings(model.version, "1.0");
|
|
// metadata expectations
|
|
// try expect(std.mem.eql(u8, model.version, "version 1.0"));
|
|
|
|
// shape expectations
|
|
try std.testing.expectEqual(intrinsic_type_count + 81, model.shapes.len);
|
|
var optsvc: ?ShapeInfo = null;
|
|
for (model.shapes) |shape| {
|
|
if (std.mem.eql(u8, shape.id, "com.amazonaws.sts#AWSSecurityTokenServiceV20110615")) {
|
|
optsvc = shape;
|
|
break;
|
|
}
|
|
}
|
|
try std.testing.expect(optsvc != null);
|
|
const svc = optsvc.?;
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts#AWSSecurityTokenServiceV20110615", svc.id);
|
|
try std.testing.expectEqualStrings("com.amazonaws.sts", svc.namespace);
|
|
try std.testing.expectEqualStrings("AWSSecurityTokenServiceV20110615", svc.name);
|
|
try std.testing.expectEqualStrings("2011-06-15", svc.shape.service.version);
|
|
// Should be 6, but we don't handle title or xml namespace
|
|
try std.testing.expectEqual(@as(usize, 4), svc.shape.service.traits.len);
|
|
try std.testing.expectEqual(@as(usize, 8), svc.shape.service.operations.len);
|
|
}
|