///|
fn found(value : Json) -> String {
match value {
Null => "null"
True | False => "a boolean"
Number(_, ..) => "a number"
String(s) => Json::string(s).stringify()
Array(_) => "an array"
Object(_) => "an object"
}
}
///|
fn[T] invalid(
path : @json.JsonPath,
message : String,
) -> T raise @json.JsonDecodeError {
raise @json.JsonDecodeError((path, message))
}
///|
fn object_value(
value : Json,
path : @json.JsonPath,
) -> Map[String, Json] raise @json.JsonDecodeError {
guard value is Object(fields) else {
invalid(path, "I expected a JSON AST object, but found \{found(value)}.")
}
fields
}
///|
fn check_fields(
fields : Map[String, Json],
allowed : ArrayView[String],
path : @json.JsonPath,
) -> Unit raise @json.JsonDecodeError {
for key, _ in fields {
if !allowed.contains(key) {
invalid(
path.add_key(key),
"I found the unknown JSON AST property \{Json::string(key).stringify()}.",
)
}
}
}
///|
fn[T : @json.FromJson] required(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T raise @json.JsonDecodeError {
let path = path.add_key(key)
guard fields.get(key) is Some(value) else {
invalid(
path,
"I expected the JSON AST property \{Json::string(key).stringify()}, but found no property.",
)
}
@json.from_json(value, path~)
}
///|
/// Explicit null is not absence: decode the inner type rather than Option.
fn[T : @json.FromJson] optional(
fields : Map[String, Json],
key : String,
path : @json.JsonPath,
) -> T? raise @json.JsonDecodeError {
match fields.get(key) {
None => None
Some(value) => Some(@json.from_json(value, path=path.add_key(key)))
}
}
///|
fn identifier(text : String) -> Bool {
let mut first = true
for c in text.iter() {
let alpha = c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
if !alpha && (first || c < '0' || c > '9') {
return false
}
first = false
}
!first
}
///|
fn absolute_id(
text : String,
path : @json.JsonPath,
) -> Unit raise @json.JsonDecodeError {
let parts = text.split("#").to_array()
let valid = if parts.length() != 2 {
false
} else {
let names = parts[1].split("$").to_array()
parts[0].split(".").all(s => identifier(s.to_owned())) &&
names.length() <= 2 &&
names.iter().all(s => identifier(s.to_owned()))
}
if !valid {
invalid(
path,
"I expected an absolute JSON AST shape ID, but found \{Json::string(text).stringify()}.",
)
}
}
///|
fn[T] check_ids(
values : Map[String, T]?,
path : @json.JsonPath,
) -> Unit raise @json.JsonDecodeError {
if values is Some(values) {
for key, _ in values {
absolute_id(key, path.add_key(key))
}
}
}
///|
fn traits(
fields : Map[String, Json],
path : @json.JsonPath,
) -> Map[String, Json]? raise @json.JsonDecodeError {
let values = optional(fields, "traits", path)
check_ids(values, path.add_key("traits"))
values
}
///|
fn members(
fields : Map[String, Json],
path : @json.JsonPath,
) -> Map[String, JsonMember]? raise @json.JsonDecodeError {
let values : Map[String, JsonMember]? = optional(fields, "members", path)
if values is Some(values) {
for name, _ in values {
if !identifier(name) {
invalid(
path.add_key("members").add_key(name),
"I expected a JSON AST member name, but found \{Json::string(name).stringify()}.",
)
}
}
}
values
}
///|
pub impl @json.FromJson for JsonAst with fn from_json(value, path) {
let fields = object_value(value, path)
check_fields(fields, ["smithy", "metadata", "shapes"], path)
let smithy = required(fields, "smithy", path)
let metadata = optional(fields, "metadata", path)
let shapes = optional(fields, "shapes", path)
check_ids(shapes, path.add_key("shapes"))
{ smithy, metadata, shapes, }
}
///|
pub impl @json.FromJson for JsonReference with fn from_json(value, path) {
let fields = object_value(value, path)
check_fields(fields, ["target"], path)
let target = required(fields, "target", path)
absolute_id(target, path.add_key("target"))
{ target, }
}
///|
pub impl @json.FromJson for JsonMember with fn from_json(value, path) {
let fields = object_value(value, path)
check_fields(fields, ["target", "traits"], path)
let target = required(fields, "target", path)
absolute_id(target, path.add_key("target"))
{ target, traits: traits(fields, path), }
}
///|
pub impl @json.FromJson for JsonShape with fn from_json(value, path) {
let fields = object_value(value, path)
let type_ : String = required(fields, "type", path)
let common : Array[String] = if type_ == "apply" {
["type", "traits"]
} else {
["type", "traits", "mixins"]
}
let extra : Array[String] = match type_ {
"list" | "set" => ["member"]
"map" => ["key", "value"]
"structure" | "union" | "enum" | "intEnum" => ["members"]
"operation" => ["input", "output", "errors"]
"service" => ["version", "operations", "resources", "errors", "rename"]
"resource" =>
[
"identifiers", "properties", "create", "put", "read", "update", "delete",
"list", "operations", "collectionOperations", "resources",
]
"apply" => []
_ => {
if @ast.SimpleType::of(type_) is None {
invalid(
path.add_key("type"),
"I expected a JSON AST shape type, but found \{Json::string(type_).stringify()}.",
)
}
[]
}
}
common.append(extra)
check_fields(fields, common[:], path)
let kind : JsonShapeKind = match type_ {
"list" => List(optional(fields, "member", path))
"set" => Set(optional(fields, "member", path))
"map" =>
Map(
key=optional(fields, "key", path),
value=optional(fields, "value", path),
)
"structure" => Structure(members(fields, path))
"union" => Union(members(fields, path))
"enum" => Enum(members(fields, path))
"intEnum" => IntEnum(members(fields, path))
"operation" => Operation(decode_operation(fields, path))
"service" => Service(decode_service(fields, path))
"resource" => Resource(decode_resource(fields, path))
"apply" => Apply
_ =>
match @ast.SimpleType::of(type_) {
Some(t) => Simple(t)
None =>
invalid(
path.add_key("type"),
"I found an unknown JSON AST shape type.",
)
}
}
{
kind,
traits: traits(fields, path),
mixins: optional(fields, "mixins", path),
}
}
///|
fn decode_operation(
fields : Map[String, Json],
path : @json.JsonPath,
) -> JsonOperation raise @json.JsonDecodeError {
{
input: optional(fields, "input", path),
output: optional(fields, "output", path),
errors: optional(fields, "errors", path),
}
}
///|
fn decode_service(
fields : Map[String, Json],
path : @json.JsonPath,
) -> JsonService raise @json.JsonDecodeError {
let rename : Map[String, String]? = optional(fields, "rename", path)
check_ids(rename, path.add_key("rename"))
if rename is Some(rename) {
for id, name in rename {
if !identifier(name) {
invalid(
path.add_key("rename").add_key(id),
"I expected a JSON AST service rename identifier, but found \{Json::string(name).stringify()}.",
)
}
}
}
{
version: optional(fields, "version", path),
operations: optional(fields, "operations", path),
resources: optional(fields, "resources", path),
errors: optional(fields, "errors", path),
rename,
}
}
///|
fn decode_resource(
fields : Map[String, Json],
path : @json.JsonPath,
) -> JsonResource raise @json.JsonDecodeError {
{
identifiers: optional(fields, "identifiers", path),
properties: optional(fields, "properties", path),
create: optional(fields, "create", path),
put: optional(fields, "put", path),
read: optional(fields, "read", path),
update: optional(fields, "update", path),
delete: optional(fields, "delete", path),
list: optional(fields, "list", path),
operations: optional(fields, "operations", path),
collectionOperations: optional(fields, "collectionOperations", path),
resources: optional(fields, "resources", path),
}
}
///|
/// Decode a core JSON value. Errors carry a JSON Pointer to the bad field.
pub fn decode(value : Json) -> JsonAst raise @json.JsonDecodeError {
@json.from_json(value)
}
///|
/// Parse JSON text and decode its document. JSON syntax errors and AST
/// structure errors retain their standard core error types. The JSON parser
/// bounds nesting before decoding; this codec does not recurse through values.
pub fn parse(text : String) -> JsonAst raise {
decode(parse_exact(text))
}