///|
/// A loader that resolves the bundled smithy.rpc and smithy.serialization traits.
fn model_loader(protocol_tests~ : Bool = false) -> @smithy.Loader {
@rpc.loader(protocol_tests~).add_builtin_idl(
@codec_contract.trait_source,
@codec_contract.trait_definitions(),
)
}
///|
let json_codec_settings : Array[String] = [
"action", "shape", "bundle", "profile", "outputProfile", "values",
]
///|
fn codec_build_error(message : String) -> @build.BuildError {
@build.InvalidBuildConfig("jsonCodec: " + message)
}
///|
/// The `jsonCodec` build plugin. Its settings select a declared root or bundle and a profile, and name
/// the value files; they cannot change a profile's rules. `check` writes a report and fails the build on
/// an invalid value; `canonicalize` writes each value in canonical form. Inputs resolve relative to the
/// build configuration.
fn json_codec_plugin(
base : String,
model : @smithy.Model,
settings : Json,
read~ : (String) -> String raise = path => @fs.read_file_to_string(path),
) -> Map[String, String] raise @build.BuildError {
guard settings is Object(fields) else {
raise codec_build_error("settings must be an object")
}
for name, _ in fields {
if !json_codec_settings.contains(name) {
raise codec_build_error(
"unknown setting `\{name}`; settings select declared profiles and cannot change their representation rules",
)
}
}
let text = fn(name : String) raise @build.BuildError {
match fields.get(name) {
None => None
Some(String(value)) => Some(value)
_ => raise codec_build_error("setting `\{name}` must be a string")
}
}
let canonicalize = match text("action") {
Some("check") => false
Some("canonicalize") => true
_ =>
raise codec_build_error("setting `action` must be check or canonicalize")
}
let (shape, bundle, profile) = (
text("shape"),
text("bundle"),
text("profile"),
)
let selection = codec_selection(shape, bundle, profile) catch {
CliError(message, _) => raise codec_build_error(message)
}
let output_profile = match text("outputProfile") {
None => None
Some(value) =>
Some(
@codec_contract.ProfileId::parse(value) catch {
_ =>
raise codec_build_error(
"expected a profile identifier in `outputProfile`",
)
},
)
}
guard fields.get("values") is Some(Array(entries)) && !entries.is_empty() else {
raise codec_build_error("setting `values` must list at least one value")
}
let values : Array[(String, String)] = []
for entry in entries {
guard entry is Object(value) && value.get("input") is Some(String(input)) else {
raise codec_build_error("each value needs an `input` path")
}
for name, _ in value {
if name != "input" && name != "output" {
raise codec_build_error("unknown value setting `\{name}`")
}
}
let input_path : @path.Path = input
if input_path.is_absolute() ||
!build_path_contains(base, build_path(base, input)) {
raise codec_build_error(
"value `\{input}` must be a path inside the build configuration directory",
)
}
let output = match value.get("output") {
None => input_path.basename().to_owned()
Some(String(name)) => name
Some(_) => raise codec_build_error("value `output` must be a string")
}
values.push((input, output))
}
let limits = @codec.CodecLimits::default()
let adapters = host_adapters()
let run : (String) -> Result[String, @codec.CodecResult] = match selection {
NodeSelection(root, profile) => {
if output_profile is Some(_) {
raise codec_build_error("`outputProfile` needs `bundle`")
}
let codec = @codec.PreparedCodec::prepare(
model,
root,
profile~,
adapters~,
limits~,
) catch {
error => raise codec_build_error(error.message())
}
fn(input) {
if canonicalize {
let out = codec.canonicalize(input) catch {
@codec.CodecFailure(result) => return Err(result)
}
Ok(out)
} else {
let result = codec.check(input)
if result.is_valid() {
Ok("")
} else {
Err(result)
}
}
}
}
BundleSelection(id, profile) => {
let bundle = @codec.PreparedBundle::prepare(model, id, adapters~, limits~) catch {
error => raise codec_build_error(error.message())
}
fn(input) {
if canonicalize {
let out = bundle.canonicalize(input, profile~, output=output_profile) catch {
@codec.CodecFailure(result) => return Err(result)
}
Ok(out)
} else {
let result = bundle.check(input, profile~)
if result.is_valid() {
Ok("")
} else {
Err(result)
}
}
}
}
}
let files : Map[String, String] = Map([])
let report : Array[Json] = []
let failures : Array[String] = []
for entry in values {
let (input, output) = entry
let content = read(build_path(base, input)) catch {
_ => raise codec_build_error("cannot read value `\{input}`")
}
match run(content) {
Ok(canonical) => {
report.push(
Json::object({
"input": Json::string(input),
"valid": Json::boolean(true),
}),
)
if canonicalize {
if files.contains(output) {
raise codec_build_error("two values write the output `\{output}`")
}
files[output] = canonical + "\n"
}
}
Err(result) => {
report.push(
Json::object({
"input": Json::string(input),
"result": result.to_json(),
}),
)
for finding in result.findings {
failures.push(
"\{input}\{finding.path.pointer()}: error[\{finding.code}] \{finding.message}",
)
}
if result.findings.is_empty() {
failures.push("\{input}: incomplete")
}
}
}
}
if !failures.is_empty() {
raise codec_build_error("invalid values:\n" + failures.join("\n"))
}
if !canonicalize {
files["json-codec-check.json"] = Json::array(report).stringify(indent=2) +
"\n"
}
files
}
///|
/// Generators and exporters keep their standard contracts. A request for a serialization profile is
/// refused rather than answered with an object-only contract under that profile's name.
fn refuse_profile(
name : String,
plugin : (@smithy.Model, Json) -> Map[String, String] raise @build.BuildError,
) -> (@smithy.Model, Json) -> Map[String, String] raise @build.BuildError {
(model, settings) => {
if settings is Object(fields) {
for key in ["profile", "serializationProfile", "jsonProfile", "bundle"] {
if fields.contains(key) {
raise @build.InvalidBuildConfig(
"\{name} does not support serialization profiles; remove `\{key}`. Use the jsonCodec plugin to check or canonicalize profile JSON.",
)
}
}
}
plugin(model, settings)
}
}